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 React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import Input from 'components/Input';
|
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 styled from 'styled-components/native';
|
||||||
import { useTypedSelector } from 'store';
|
import { useTypedSelector } from 'store';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
@@ -9,7 +9,6 @@ import { FlatList } from 'react-native-gesture-handler';
|
|||||||
import TouchableHandler from 'components/TouchableHandler';
|
import TouchableHandler from 'components/TouchableHandler';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
import { MusicNavigationProp } from '../types';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import useDefaultStyles from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
@@ -17,23 +16,33 @@ import { searchAndFetchAlbums } from 'store/music/actions';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Text } from 'components/Typography';
|
import { Text } from 'components/Typography';
|
||||||
|
import { MusicNavigationProp } from 'screens/Music/types';
|
||||||
import DownloadIcon from 'components/DownloadIcon';
|
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 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`
|
const Container = styled.View`
|
||||||
padding: 0 32px;
|
padding: 4px 32px 0 32px;
|
||||||
position: relative;
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-top-width: 1px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FullSizeContainer = styled(Container)`
|
const FullSizeContainer = styled.View`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Loading = styled.View`
|
const Loading = styled.View`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 32px;
|
right: 12px;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -61,10 +70,18 @@ const SearchResult = styled.View`
|
|||||||
height: 54px;
|
height: 54px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fuseOptions = {
|
const SearchIndicator = styled(SearchIcon)`
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 26px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const fuseOptions: Fuse.IFuseOptions<Album> = {
|
||||||
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
|
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
|
fieldNormWeight: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
type AudioResult = {
|
type AudioResult = {
|
||||||
@@ -86,17 +103,14 @@ type CombinedResults = (AudioResult | AlbumResult)[];
|
|||||||
export default function Search() {
|
export default function Search() {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
|
|
||||||
// Prepare state
|
// Prepare state for fuse and albums
|
||||||
const [fuseIsReady, setFuseReady] = useState(false);
|
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>>();
|
const fuse = useRef<Fuse<Album>>();
|
||||||
const searchElement = useRef<TextInput>(null);
|
|
||||||
|
|
||||||
// Prepare helpers
|
// Prepare helpers
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<MusicNavigationProp>();
|
||||||
@@ -197,40 +211,62 @@ 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]
|
||||||
);
|
);
|
||||||
|
|
||||||
const HeaderComponent = React.useMemo(() => (
|
const HeaderComponent = React.useMemo(() => (
|
||||||
<Container>
|
<View>
|
||||||
|
<Container style={defaultStyles.border}>
|
||||||
|
<View>
|
||||||
<Input
|
<Input
|
||||||
ref={searchElement}
|
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChangeText={setSearchTerm}
|
onChangeText={setSearchTerm}
|
||||||
style={defaultStyles.input}
|
style={[defaultStyles.input, { marginBottom: 12 }]}
|
||||||
placeholder={t('search') + '...'}
|
placeholder={t('search') + '...'}
|
||||||
/>
|
/>
|
||||||
<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} style={{ position: 'absolute', left: 48, top: 26}} />
|
<SearchIndicator width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />
|
||||||
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
{isLoading && <Loading><ActivityIndicator /></Loading>}
|
||||||
|
</View>
|
||||||
</Container>
|
</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]);
|
), [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.
|
// 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.
|
||||||
if (!fuseIsReady) {
|
if (!fuseIsReady) {
|
||||||
@@ -240,7 +276,7 @@ export default function Search() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 2 }}
|
||||||
data={[...jellyfinResults, ...fuseResults]}
|
data={[...jellyfinResults, ...fuseResults]}
|
||||||
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
||||||
const album = albums[trackAlbum || id];
|
const album = albums[trackAlbum || id];
|
||||||
@@ -254,7 +290,9 @@ export default function Search() {
|
|||||||
return (
|
return (
|
||||||
<TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
|
<TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
|
||||||
<SearchResult>
|
<SearchResult>
|
||||||
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
<ShadowWrapper>
|
||||||
|
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
|
||||||
|
</ShadowWrapper>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text numberOfLines={1}>
|
<Text numberOfLines={1}>
|
||||||
{trackName || album.Name}
|
{trackName || album.Name}
|
||||||
@@ -277,8 +315,6 @@ export default function Search() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
ListHeaderComponent={HeaderComponent}
|
|
||||||
// ListFooterComponent={FooterComponent}
|
|
||||||
extraData={[searchTerm, albums]}
|
extraData={[searchTerm, albums]}
|
||||||
/>
|
/>
|
||||||
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
{(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>
|
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||||
</FullSizeContainer>
|
</FullSizeContainer>
|
||||||
) : null}
|
) : null}
|
||||||
|
{HeaderComponent}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user