First overhaul of search screen

This commit is contained in:
Lei Nelissen
2022-05-11 23:57:30 +02:00
parent b7a5c0267c
commit d7402bb409
10 changed files with 103 additions and 67 deletions

View File

@@ -7,13 +7,29 @@ import store, { persistedStore } from 'store';
import { import {
NavigationContainer, NavigationContainer,
DefaultTheme, DefaultTheme,
DarkTheme, DarkTheme as BaseDarkTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { useColorScheme } from 'react-native'; import { useColorScheme } from 'react-native';
import { ColorSchemeContext, themes } from './Colors'; import { ColorSchemeContext, themes } from './Colors';
import DownloadManager from './DownloadManager'; import DownloadManager from './DownloadManager';
// import ErrorReportingAlert from 'utility/ErrorReportingAlert'; // import ErrorReportingAlert from 'utility/ErrorReportingAlert';
const LightTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: themes.light.view.backgroundColor,
}
};
const DarkTheme = {
...BaseDarkTheme,
colors: {
...BaseDarkTheme.colors,
background: themes.light.view.backgroundColor,
}
};
export default function App(): JSX.Element { export default function App(): JSX.Element {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
// const colorScheme = 'dark'; // const colorScheme = 'dark';
@@ -41,7 +57,9 @@ export default function App(): JSX.Element {
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistedStore}> <PersistGate loading={null} persistor={persistedStore}>
<ColorSchemeContext.Provider value={theme}> <ColorSchemeContext.Provider value={theme}>
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <NavigationContainer
theme={colorScheme === 'dark' ? DarkTheme : LightTheme}
>
<Routes /> <Routes />
<DownloadManager /> <DownloadManager />
</NavigationContainer> </NavigationContainer>

View File

@@ -48,13 +48,9 @@ function generateStyles(scheme: ColorSchemeName) {
backgroundColor: scheme === 'dark' ? '#161616' : '#eee', backgroundColor: scheme === 'dark' ? '#161616' : '#eee',
}, },
input: { input: {
backgroundColor: scheme === 'dark' ? '#161616' : '#e6e6e6', backgroundColor: scheme === 'dark' ? '#161616' : '#f3f3f3',
color: scheme === 'dark' ? '#fff' : '#000', color: scheme === 'dark' ? '#fff' : '#000',
}, },
sectionHeading: {
backgroundColor: scheme === 'dark' ? '#111' : '#eee',
borderColor: scheme === 'dark' ? '#333' : '#ddd',
},
stackHeader: { stackHeader: {
color: scheme === 'dark' ? 'white' : 'black' color: scheme === 'dark' ? 'white' : 'black'
}, },

View File

@@ -4,6 +4,7 @@ const Input = styled.TextInput`
margin: 10px 0; margin: 10px 0;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
padding-left: 40px;
`; `;
export default Input; export default Input;

View File

@@ -1,30 +1,24 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { TouchableOpacityProps } from 'react-native'; import { TouchableOpacityProps } from 'react-native';
import ChevronRight from 'assets/icons/chevron-right.svg'; import ChevronRight from 'assets/icons/chevron-right.svg';
import styled, { css } from 'styled-components/native'; import styled from 'styled-components/native';
import { THEME_COLOR } from 'CONSTANTS'; import { THEME_COLOR } from 'CONSTANTS';
import useDefaultStyles from './Colors'; import useDefaultStyles from './Colors';
const BUTTON_SIZE = 14; const BUTTON_SIZE = 14;
const Container = styled.Pressable<{ active?: boolean }>` const Container = styled.Pressable<{ active?: boolean }>`
padding: 18px 20px; padding: 14px 16px;
border-bottom-width: 1px; border-radius: 8px;
margin: 4px 8px;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center;
${props => props.active && css`
background-color: ${THEME_COLOR};
`}
`; `;
const Label = styled.Text<{ active?: boolean }>` const Label = styled.Text<{ active?: boolean }>`
color: ${THEME_COLOR}; color: ${THEME_COLOR};
font-size: 16px; font-size: 16px;
${props => props.active && css`
color: white;
`}
`; `;
const ListButton: React.FC<TouchableOpacityProps> = ({ children, ...props }) => { const ListButton: React.FC<TouchableOpacityProps> = ({ children, ...props }) => {
@@ -34,16 +28,17 @@ const ListButton: React.FC<TouchableOpacityProps> = ({ children, ...props }) =>
const handlePressOut = useCallback(() => setPressed(false), []); const handlePressOut = useCallback(() => setPressed(false), []);
return ( return (
// @ts-expect-error styled-components has outdated react-native typings
<Container <Container
{...props} {...props}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={defaultStyles.border} style={[
active={isPressed} defaultStyles.border,
isPressed ? defaultStyles.activeBackground : undefined
]}
> >
<Label active={isPressed}>{children}</Label> <Label>{children}</Label>
<ChevronRight width={BUTTON_SIZE} height={BUTTON_SIZE} fill={isPressed ? '#fff' : THEME_COLOR} /> <ChevronRight width={BUTTON_SIZE} height={BUTTON_SIZE} fill={THEME_COLOR} />
</Container> </Container>
); );
}; };

View File

@@ -22,7 +22,6 @@ const DownloadedTrack = styled.View`
padding: 8px 0; padding: 8px 0;
align-items: center; align-items: center;
margin: 0 20px; margin: 0 20px;
border-bottom-width: 1px;
`; `;
function Downloads() { function Downloads() {
@@ -87,7 +86,7 @@ function Downloads() {
), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]); ), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]);
const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => ( const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => (
<DownloadedTrack style={defaultStyles.border}> <DownloadedTrack>
<View style={{ marginRight: 12 }}> <View style={{ marginRight: 12 }}>
<DownloadIcon trackId={item} /> <DownloadIcon trackId={item} />
</View> </View>

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, ReactText } from 'react'; import React, { useCallback, useEffect, useRef, ReactText } from 'react';
import { useGetImage } from 'utility/JellyfinApi'; import { useGetImage } from 'utility/JellyfinApi';
import { MusicNavigationProp } from '../types'; import { MusicNavigationProp } from '../types';
import { Text, SafeAreaView, SectionList, View } from 'react-native'; import { SafeAreaView, SectionList, View } from 'react-native';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
@@ -9,13 +9,16 @@ import { useTypedSelector } from 'store';
import { fetchAllAlbums } from 'store/music/actions'; import { fetchAllAlbums } from 'store/music/actions';
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS'; import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
import TouchableHandler from 'components/TouchableHandler'; import TouchableHandler from 'components/TouchableHandler';
import AlbumImage, { AlbumItem } from './components/AlbumImage'; import AlbumImage, { AlbumHeight, AlbumItem } from './components/AlbumImage';
import { selectAlbumsByAlphabet, SectionedId } from 'store/music/selectors'; import { selectAlbumsByAlphabet, SectionedId } from 'store/music/selectors';
import AlphabetScroller from 'components/AlphabetScroller'; import AlphabetScroller from 'components/AlphabetScroller';
import { EntityId } from '@reduxjs/toolkit'; import { EntityId } from '@reduxjs/toolkit';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
import { Album } from 'store/music/types'; import { Album } from 'store/music/types';
import { Text } from 'components/Typography';
const HeadingHeight = 50;
interface VirtualizedItemInfo { interface VirtualizedItemInfo {
section: SectionedId, section: SectionedId,
@@ -40,25 +43,25 @@ function generateSection({ section }: { section: SectionedId }) {
} }
const SectionContainer = styled.View` const SectionContainer = styled.View`
border-bottom-width: 1px; height: ${HeadingHeight}px;
height: 50px;
justify-content: center; justify-content: center;
padding: 0 10px; padding: 0 24px;
`; `;
const SectionText = styled.Text` const SectionText = styled(Text)`
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: 400;
`; `;
const SectionHeading = React.memo(function SectionHeading(props: { label: string }) { const SectionHeading = React.memo(function SectionHeading(props: { label: string }) {
const defaultStyles = useDefaultStyles();
const { label } = props; const { label } = props;
return ( return (
<SectionContainer style={defaultStyles.sectionHeading}> <ColoredBlurView>
<SectionText style={defaultStyles.text}>{label}</SectionText> <SectionContainer>
</SectionContainer> <SectionText>{label}</SectionText>
</SectionContainer>
</ColoredBlurView>
); );
}); });
@@ -118,21 +121,20 @@ const Albums: React.FC = () => {
// We can then determine the "length" (=height) of this item. Header items // We can then determine the "length" (=height) of this item. Header items
// end up with an itemIndex of -1, thus are easy to identify. // end up with an itemIndex of -1, thus are easy to identify.
const length = header ? 50 : (itemIndex % 2 === 0 ? 220 : 0); const length = header ? 50 : (itemIndex % 2 === 0 ? AlbumHeight : 0);
// We'll also need to account for any unevenly-ended lists up until the // We'll also need to account for any unevenly-ended lists up until the
// current item. // current item.
const previousRows = data?.filter((row, i) => i < sectionIndex) const previousRows = data?.filter((row, i) => i < sectionIndex)
.reduce((sum, row) => sum + Math.ceil(row.data.length / 2), 0) || 0; .reduce((sum, row) => sum + Math.ceil(row.data.length / 2), 0) || 0;
// We must also calcuate the offset, total distance from the top of the // We must also calcuate the offset, total distance from the top of the
// screen. First off, we'll account for each sectionIndex that is shown up // screen. First off, we'll account for each sectionIndex that is shown up
// until now. This only includes the heading for the current section if the // until now. This only includes the heading for the current section if the
// item is not the section header // item is not the section header
const headingOffset = 50 * (header ? sectionIndex : sectionIndex + 1); const headingOffset = HeadingHeight * (header ? sectionIndex : sectionIndex + 1);
const currentRows = itemIndex > 1 ? Math.ceil((itemIndex + 1) / 2) : 0; const currentRows = itemIndex > 1 ? Math.ceil((itemIndex + 1) / 2) : 0;
const itemOffset = 220 * (previousRows + currentRows); const itemOffset = AlbumHeight * (previousRows + currentRows);
const offset = headingOffset + itemOffset; const offset = headingOffset + itemOffset;
return { index, length, offset }; return { index, length, offset };
@@ -189,7 +191,7 @@ const Albums: React.FC = () => {
onRefresh={retrieveData} onRefresh={retrieveData}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
ref={listRef} ref={listRef}
keyExtractor={(item, index) => `${item}_${index}`} keyExtractor={(item) => item as string}
renderSectionHeader={generateSection} renderSectionHeader={generateSection}
renderItem={generateItem} renderItem={generateItem}
/> />

View File

@@ -15,17 +15,23 @@ import ListButton from 'components/ListButton';
import { t } from '@localisation'; import { t } from '@localisation';
import useDefaultStyles from 'components/Colors'; import useDefaultStyles from 'components/Colors';
import { Album } from 'store/music/types'; import { Album } from 'store/music/types';
import Divider from 'components/Divider';
import styled from 'styled-components/native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
columnWrapper: { columnWrapper: {
paddingLeft: 10, paddingHorizontal: 16,
paddingRight: 10
} }
}); });
const HeaderContainer = styled.View`
display: flex;
flex-direction: row;
align-items: center;
`;
const NavigationHeader: React.FC = () => { const NavigationHeader: React.FC = () => {
const navigation = useNavigation<MusicNavigationProp>(); const navigation = useNavigation<MusicNavigationProp>();
const defaultStyles = useDefaultStyles();
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]); const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]); const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
@@ -34,7 +40,10 @@ const NavigationHeader: React.FC = () => {
<ListButton onPress={handleAllAlbumsClick}>{t('all-albums')}</ListButton> <ListButton onPress={handleAllAlbumsClick}>{t('all-albums')}</ListButton>
<ListButton onPress={handlePlaylistsClick}>{t('playlists')}</ListButton> <ListButton onPress={handlePlaylistsClick}>{t('playlists')}</ListButton>
<ListContainer> <ListContainer>
<Header style={defaultStyles.text}>{t('recent-albums')}</Header> <HeaderContainer>
<Header>{t('recent-albums')}</Header>
<Divider style={{ marginLeft: 24 }} />
</HeaderContainer>
</ListContainer> </ListContainer>
</> </>
); );

View File

@@ -3,17 +3,20 @@ import FastImage from 'react-native-fast-image';
import { Dimensions } from 'react-native'; import { Dimensions } from 'react-native';
const Screen = Dimensions.get('screen'); const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24;
export const AlbumHeight = AlbumWidth + 40;
export const CoverSize = AlbumWidth - 16;
export const AlbumItem = styled.View` export const AlbumItem = styled.View`
width: ${Screen.width / 2 - 10}px; width: ${AlbumWidth}px;
padding: 10px; height: ${AlbumHeight}px;
height: 220px; padding: 8px;
`; `;
const AlbumImage = styled(FastImage)` const AlbumImage = styled(FastImage)`
border-radius: 10px; border-radius: 10px;
width: ${Screen.width / 2 - 40}px; width: ${CoverSize}px;
height: ${Screen.width / 2 - 40}px; height: ${CoverSize}px;
margin-bottom: 5px; margin-bottom: 5px;
`; `;

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components/native'; import styled from 'styled-components/native';
const ListContainer = styled.View` const ListContainer = styled.View`
padding: 10px; padding: 24px;
`; `;
export default ListContainer; export default ListContainer;

View File

@@ -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, Text, TextInput, View } from 'react-native'; import { ActivityIndicator, SafeAreaView, TextInput, 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';
@@ -16,9 +16,14 @@ import useDefaultStyles from 'components/Colors';
import { searchAndFetchAlbums } from 'store/music/actions'; 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 DownloadIcon from 'components/DownloadIcon';
import ChevronRight from 'assets/icons/chevron-right.svg';
import SearchIcon from 'assets/icons/magnifying-glass.svg';
const Container = styled.View` const Container = styled.View`
padding: 0 20px; padding: 0 32px;
position: relative; position: relative;
`; `;
@@ -37,8 +42,8 @@ const Loading = styled.View`
const AlbumImage = styled(FastImage)` const AlbumImage = styled(FastImage)`
border-radius: 4px; border-radius: 4px;
width: 25px; width: 32px;
height: 25px; height: 32px;
margin-right: 10px; margin-right: 10px;
`; `;
@@ -46,15 +51,14 @@ const HalfOpacity = styled.Text`
opacity: 0.5; opacity: 0.5;
margin-top: 2px; margin-top: 2px;
font-size: 12px; font-size: 12px;
flex: 1 1 auto;
`; `;
const SearchResult = styled.View` const SearchResult = styled.View`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
border-bottom-width: 1px; padding: 8px 32px;
margin-left: 15px; height: 54px;
padding-right: 15px;
height: 50px;
`; `;
const fuseOptions = { const fuseOptions = {
@@ -208,13 +212,13 @@ export default function Search() {
const HeaderComponent = React.useMemo(() => ( const HeaderComponent = React.useMemo(() => (
<Container> <Container>
<Input <Input
// @ts-expect-error styled-components has outdated react-native typings
ref={searchElement} ref={searchElement}
value={searchTerm} value={searchTerm}
onChangeText={setSearchTerm} onChangeText={setSearchTerm}
style={defaultStyles.input} style={defaultStyles.input}
placeholder={t('search') + '...'} placeholder={t('search') + '...'}
/> />
<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} style={{ position: 'absolute', left: 48, top: 26}} />
{isLoading && <Loading><ActivityIndicator /></Loading>} {isLoading && <Loading><ActivityIndicator /></Loading>}
</Container> </Container>
), [searchTerm, setSearchTerm, defaultStyles, isLoading]); ), [searchTerm, setSearchTerm, defaultStyles, isLoading]);
@@ -249,16 +253,25 @@ export default function Search() {
return ( return (
<TouchableHandler<string> id={album.Id} onPress={selectAlbum}> <TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
<SearchResult style={defaultStyles.border}> <SearchResult>
<AlbumImage source={{ uri: getImage(album.Id) }} /> <AlbumImage source={{ uri: getImage(album.Id) }} />
<View> <View style={{ flex: 1 }}>
<Text numberOfLines={1} ellipsizeMode="tail" style={defaultStyles.text}> <Text numberOfLines={1}>
{trackName || album.Name} - {album.AlbumArtist} {trackName || album.Name}
</Text> </Text>
<HalfOpacity style={defaultStyles.text}> <HalfOpacity style={defaultStyles.text} numberOfLines={1}>
{type === 'AlbumArtist' ? t('album'): t('track')} {type === 'AlbumArtist'
? `${t('album')}${album.AlbumArtist}`
: `${t('track')}${album.AlbumArtist}${album.Name}`
}
</HalfOpacity> </HalfOpacity>
</View> </View>
<View style={{ marginLeft: 16 }}>
<DownloadIcon trackId={id} />
</View>
<View style={{ marginLeft: 16 }}>
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
</View>
</SearchResult> </SearchResult>
</TouchableHandler> </TouchableHandler>
); );