Add buttons for deleting downloaded tracks from playlist or album
This commit is contained in:
@@ -20,6 +20,10 @@ const BaseButton = styled.Pressable`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
${(props) => props.disabled && css`
|
||||||
|
opacity: 0.25;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ButtonText = styled.Text<{ active?: boolean }>`
|
const ButtonText = styled.Text<{ active?: boolean }>`
|
||||||
@@ -32,7 +36,7 @@ const ButtonText = styled.Text<{ active?: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
|
const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
|
||||||
const { icon: Icon, title, ...rest } = props;
|
const { icon: Icon, title, disabled, ...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), []);
|
||||||
@@ -41,6 +45,7 @@ const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
|
|||||||
return (
|
return (
|
||||||
<BaseButton
|
<BaseButton
|
||||||
{...rest}
|
{...rest}
|
||||||
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onPressIn={handlePressIn}
|
onPressIn={handlePressIn}
|
||||||
onPressOut={handlePressOut}
|
onPressOut={handlePressOut}
|
||||||
|
|||||||
@@ -51,5 +51,7 @@
|
|||||||
"download-playlist": "Download Playlist",
|
"download-playlist": "Download Playlist",
|
||||||
"no-downloads": "You have not yet downloaded any tracks",
|
"no-downloads": "You have not yet downloaded any tracks",
|
||||||
"delete-all-tracks": "Delete All Tracks",
|
"delete-all-tracks": "Delete All Tracks",
|
||||||
|
"delete-album": "Delete Album",
|
||||||
|
"delete-playlist": "Delete Playlist",
|
||||||
"total-download-size": "Total Download Size"
|
"total-download-size": "Total Download Size"
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,7 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'download-track'
|
| 'download-track'
|
||||||
| 'download-album'
|
| 'download-album'
|
||||||
| 'download-playlist'
|
| 'download-playlist'
|
||||||
| 'delete-all-tracks'
|
| 'delete-album'
|
||||||
|
| 'delete-playlist'
|
||||||
| 'total-download-size'
|
| 'total-download-size'
|
||||||
| 'no-downloads'
|
| 'no-downloads'
|
||||||
@@ -37,7 +37,8 @@ const Album: React.FC = () => {
|
|||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
playButtonText={t('play-album')}
|
playButtonText={t('play-album')}
|
||||||
shuffleButtonText={t('shuffle-album')}
|
shuffleButtonText={t('shuffle-album')}
|
||||||
downloadText={t('download-album')}
|
downloadButtonText={t('download-album')}
|
||||||
|
deleteButtonText={t('delete-album')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ const Playlist: React.FC = () => {
|
|||||||
listNumberingStyle='index'
|
listNumberingStyle='index'
|
||||||
playButtonText={t('play-playlist')}
|
playButtonText={t('play-playlist')}
|
||||||
shuffleButtonText={t('shuffle-playlist')}
|
shuffleButtonText={t('shuffle-playlist')}
|
||||||
downloadText={t('download-playlist')}
|
downloadButtonText={t('download-playlist')}
|
||||||
|
deleteButtonText={t('delete-playlist')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import { MusicNavigationProp } from 'screens/Music/types';
|
|||||||
import DownloadIcon from 'components/DownloadIcon';
|
import DownloadIcon from 'components/DownloadIcon';
|
||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
|
import CloudDownArrow from 'assets/cloud-down-arrow.svg';
|
||||||
|
import Trash from 'assets/trash.svg';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { downloadTrack } from 'store/downloads/actions';
|
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
||||||
|
import { selectDownloadedTracks } from 'store/downloads/selectors';
|
||||||
|
|
||||||
const Screen = Dimensions.get('screen');
|
const Screen = Dimensions.get('screen');
|
||||||
|
|
||||||
@@ -68,7 +70,8 @@ interface TrackListViewProps {
|
|||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
playButtonText: string;
|
playButtonText: string;
|
||||||
shuffleButtonText: string;
|
shuffleButtonText: string;
|
||||||
downloadText: string;
|
downloadButtonText: string;
|
||||||
|
deleteButtonText: string;
|
||||||
listNumberingStyle?: 'album' | 'index';
|
listNumberingStyle?: 'album' | 'index';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +83,8 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
refresh,
|
refresh,
|
||||||
playButtonText,
|
playButtonText,
|
||||||
shuffleButtonText,
|
shuffleButtonText,
|
||||||
downloadText,
|
downloadButtonText,
|
||||||
|
deleteButtonText,
|
||||||
listNumberingStyle = 'album',
|
listNumberingStyle = 'album',
|
||||||
}) => {
|
}) => {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
@@ -88,6 +92,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
// Retrieve state
|
// Retrieve state
|
||||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||||
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
||||||
|
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
|
||||||
|
|
||||||
// Retrieve helpers
|
// Retrieve helpers
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
@@ -110,6 +115,9 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
const downloadAllTracks = useCallback(() => {
|
const downloadAllTracks = useCallback(() => {
|
||||||
trackIds.forEach((trackId) => dispatch(downloadTrack(trackId)));
|
trackIds.forEach((trackId) => dispatch(downloadTrack(trackId)));
|
||||||
}, [dispatch, trackIds]);
|
}, [dispatch, trackIds]);
|
||||||
|
const deleteAllTracks = useCallback(() => {
|
||||||
|
downloadedTracks.forEach((trackId) => dispatch(removeDownloadedTrack(trackId)));
|
||||||
|
}, [dispatch, downloadedTracks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -162,7 +170,20 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
</TrackContainer>
|
</TrackContainer>
|
||||||
</TouchableHandler>
|
</TouchableHandler>
|
||||||
)}
|
)}
|
||||||
<Button icon={CloudDownArrow} title={downloadText} style={{ marginTop: 12 }} onPress={downloadAllTracks} />
|
<WrappableButtonRow style={{ marginTop: 24 }}>
|
||||||
|
<WrappableButton
|
||||||
|
icon={CloudDownArrow}
|
||||||
|
title={downloadButtonText}
|
||||||
|
onPress={downloadAllTracks}
|
||||||
|
disabled={downloadedTracks.length === trackIds.length}
|
||||||
|
/>
|
||||||
|
<WrappableButton
|
||||||
|
icon={Trash}
|
||||||
|
title={deleteButtonText}
|
||||||
|
onPress={deleteAllTracks}
|
||||||
|
disabled={downloadedTracks.length === 0}
|
||||||
|
/>
|
||||||
|
</WrappableButtonRow>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
|
|||||||
13
src/store/downloads/selectors.ts
Normal file
13
src/store/downloads/selectors.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createSelector, EntityId } from '@reduxjs/toolkit';
|
||||||
|
import { intersection } from 'lodash';
|
||||||
|
import { AppState } from 'store';
|
||||||
|
|
||||||
|
export const selectDownloadedTracks = (trackIds: EntityId[]) => (
|
||||||
|
createSelector(
|
||||||
|
(state: AppState) => state.downloads,
|
||||||
|
({ entities, ids }) => {
|
||||||
|
return intersection(trackIds, ids)
|
||||||
|
.filter((id) => entities[id]?.isComplete);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user