Restyle the downloads screen

This commit is contained in:
Lei Nelissen
2022-05-17 23:34:25 +02:00
parent c0fa1160ae
commit de07fc65c4
3 changed files with 79 additions and 40 deletions

View File

@@ -7,13 +7,16 @@ import { THEME_COLOR } from 'CONSTANTS';
import styled, { css } from 'styled-components/native'; import styled, { css } from 'styled-components/native';
import useDefaultStyles from './Colors'; import useDefaultStyles from './Colors';
type ButtonSize = 'default' | 'small';
interface ButtonProps extends PressableProps { interface ButtonProps extends PressableProps {
icon?: React.FC<SvgProps>; icon?: React.FC<SvgProps>;
title: string; title?: string;
style?: ViewProps['style']; style?: ViewProps['style'];
size?: ButtonSize;
} }
const BaseButton = styled.Pressable` const BaseButton = styled.Pressable<{ size: ButtonSize }>`
padding: 12px; padding: 12px;
border-radius: 8px; border-radius: 8px;
flex-direction: row; flex-direction: row;
@@ -24,16 +27,25 @@ const BaseButton = styled.Pressable`
${(props) => props.disabled && css` ${(props) => props.disabled && css`
opacity: 0.25; opacity: 0.25;
`} `}
${(props) => props.size === 'small' && css`
flex-grow: 0;
padding: 10px;
`}
`; `;
const ButtonText = styled.Text<{ active?: boolean }>` const ButtonText = styled.Text<{ active?: boolean, size: ButtonSize }>`
color: ${THEME_COLOR}; color: ${THEME_COLOR};
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
${(props) => props.size === 'small' && css`
font-size: 12px;
`}
`; `;
const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) { const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
const { icon: Icon, title, disabled, ...rest } = props; const { icon: Icon, title, disabled, size = 'default', ...rest } = props;
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const [isPressed, setPressed] = useState(false); const [isPressed, setPressed] = useState(false);
const handlePressIn = useCallback(() => setPressed(true), []); const handlePressIn = useCallback(() => setPressed(true), []);
@@ -48,8 +60,12 @@ const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
onPressOut={handlePressOut} onPressOut={handlePressOut}
style={[ style={[
props.style, props.style,
{ backgroundColor: isPressed ? defaultStyles.activeBackground.backgroundColor : defaultStyles.button.backgroundColor } { backgroundColor: isPressed
? defaultStyles.activeBackground.backgroundColor
: defaultStyles.button.backgroundColor
}
]} ]}
size={size}
> >
{Icon && {Icon &&
<Icon <Icon
@@ -57,11 +73,13 @@ const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
height={14} height={14}
fill={THEME_COLOR} fill={THEME_COLOR}
style={{ style={{
marginRight: 8, marginRight: title ? 8 : 0,
}} }}
/> />
} }
<ButtonText active={isPressed}>{title}</ButtonText> {title ? (
<ButtonText active={isPressed} size={size}>{title}</ButtonText>
) : undefined}
</BaseButton> </BaseButton>
); );
}); });

View File

@@ -1,13 +1,12 @@
import useDefaultStyles from 'components/Colors'; import useDefaultStyles from 'components/Colors';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { FlatListProps, Text, TouchableOpacity, View } from 'react-native'; import { FlatListProps, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler'; import { FlatList } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useTypedSelector } from 'store'; import { useTypedSelector } from 'store';
import formatBytes from 'utility/formatBytes'; import formatBytes from 'utility/formatBytes';
import TrashIcon from 'assets/icons/trash.svg'; import TrashIcon from 'assets/icons/trash.svg';
import ArrowClockwise from 'assets/icons/arrow-clockwise.svg'; import ArrowClockwise from 'assets/icons/arrow-clockwise.svg';
import { THEME_COLOR } from 'CONSTANTS';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { EntityId } from '@reduxjs/toolkit'; import { EntityId } from '@reduxjs/toolkit';
import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions'; import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions';
@@ -15,6 +14,10 @@ import Button from 'components/Button';
import { t } from 'i18n-js'; import { t } from 'i18n-js';
import DownloadIcon from 'components/DownloadIcon'; import DownloadIcon from 'components/DownloadIcon';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { Text } from 'components/Typography';
import FastImage from 'react-native-fast-image';
import { useGetImage } from 'utility/JellyfinApi';
import { ShadowWrapper } from 'components/Shadow';
const DownloadedTrack = styled.View` const DownloadedTrack = styled.View`
flex: 1 0 auto; flex: 1 0 auto;
@@ -24,9 +27,16 @@ const DownloadedTrack = styled.View`
margin: 0 20px; margin: 0 20px;
`; `;
const AlbumImage = styled(FastImage)`
height: 32px;
width: 32px;
border-radius: 4px;
`;
function Downloads() { function Downloads() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const dispatch = useDispatch(); const dispatch = useDispatch();
const getImage = useGetImage();
const { entities, ids } = useTypedSelector((state) => state.downloads); const { entities, ids } = useTypedSelector((state) => state.downloads);
const tracks = useTypedSelector((state) => state.music.tracks.entities); const tracks = useTypedSelector((state) => state.music.tracks.entities);
@@ -64,57 +74,68 @@ function Downloads() {
*/ */
const ListHeaderComponent = useMemo(() => ( const ListHeaderComponent = useMemo(() => (
<View style={{ marginHorizontal: 20, marginBottom: 12 }}> <View style={[{ paddingHorizontal: 20, paddingBottom: 12, borderBottomWidth: 0.5 }, defaultStyles.border]}>
<Text style={[{ textAlign: 'center', marginVertical: 6 }, defaultStyles.textHalfOpacity]}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
{t('total-download-size')}: {formatBytes(totalDownloadSize)} <Text
</Text> style={[
<Button defaultStyles.textHalfOpacity,
icon={TrashIcon} { marginRight: 8, flex: 1, fontSize: 12 },
title={t('delete-all-tracks')} ]}
onPress={handleDeleteAllTracks} numberOfLines={1}
disabled={!ids.length} >
style={{ marginTop: 8 }} {t('total-download-size')}: {formatBytes(totalDownloadSize)}
/> </Text>
<Button <Button
icon={ArrowClockwise} icon={TrashIcon}
title={t('retry-failed-downloads')} title={t('delete-all-tracks')}
onPress={handleRetryFailed} onPress={handleDeleteAllTracks}
disabled={failedIds.length === 0} disabled={!ids.length}
style={{ marginTop: 4 }} size="small"
/> />
</View>
{failedIds.length > 0 && (
<Button
icon={ArrowClockwise}
title={t('retry-failed-downloads')}
onPress={handleRetryFailed}
disabled={failedIds.length === 0}
style={{ marginTop: 4 }}
/>
)}
</View> </View>
), [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> <DownloadedTrack>
<View style={{ marginRight: 12 }}> <View style={{ marginRight: 12 }}>
<DownloadIcon trackId={item} /> <ShadowWrapper size="small">
<AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
</View> </View>
<View style={{ flexShrink: 1, marginRight: 8 }}> <View style={{ flexShrink: 1, marginRight: 8 }}>
<Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}> <Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
{tracks[item]?.Name} {tracks[item]?.Name}
</Text> </Text>
<Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}> <Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
{tracks[item]?.AlbumArtist} ({tracks[item]?.Album}) {tracks[item]?.AlbumArtist} {tracks[item]?.Album ? `${tracks[item]?.Album}` : ''}
</Text> </Text>
</View> </View>
<View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}> <View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
{entities[item]?.isComplete && entities[item]?.size ? ( {entities[item]?.isComplete && entities[item]?.size ? (
<Text style={[defaultStyles.textHalfOpacity, { marginRight: 6, fontSize: 12 }]}> <Text style={[defaultStyles.textQuarterOpacity, { marginRight: 12, fontSize: 12 }]}>
{formatBytes(entities[item]?.size || 0)} {formatBytes(entities[item]?.size || 0)}
</Text> </Text>
) : null} ) : null}
<TouchableOpacity onPress={() => handleDelete(item)}> <View style={{ marginRight: 12 }}>
<TrashIcon height={24} width={24} fill={THEME_COLOR} /> <DownloadIcon trackId={item} />
</TouchableOpacity> </View>
<Button onPress={() => handleDelete(item)} size="small" icon={TrashIcon} />
{!entities[item]?.isComplete && ( {!entities[item]?.isComplete && (
<TouchableOpacity onPress={() => retryTrack(item)}> <Button onPress={() => retryTrack(item)} size="small" icon={ArrowClockwise} style={{ marginLeft: 4 }} />
<ArrowClockwise height={18} width={18} fill={THEME_COLOR} />
</TouchableOpacity>
)} )}
</View> </View>
</DownloadedTrack> </DownloadedTrack>
), [entities, retryTrack, handleDelete, defaultStyles, tracks]); ), [entities, retryTrack, handleDelete, defaultStyles, tracks, getImage]);
// If no tracks have been downloaded, show a short message describing this // If no tracks have been downloaded, show a short message describing this
if (!ids.length) { if (!ids.length) {
@@ -129,11 +150,11 @@ function Downloads() {
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
{ListHeaderComponent}
<FlatList <FlatList
data={ids} data={ids}
style={{ flex: 1 }} style={{ flex: 1, paddingTop: 12 }}
contentContainerStyle={{ flexGrow: 1 }} contentContainerStyle={{ flexGrow: 1 }}
ListHeaderComponent={ListHeaderComponent}
renderItem={renderItem} renderItem={renderItem}
/> />
</SafeAreaView> </SafeAreaView>

View File

@@ -1,5 +1,5 @@
const k = 1024; const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
/** /**
* Convert a number of bytes to a human-readable string * Convert a number of bytes to a human-readable string