Allow for retring individual tracks
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTypedSelector } from 'store';
|
import { useTypedSelector } from 'store';
|
||||||
import CloudIcon from 'assets/cloud.svg';
|
import CloudIcon from 'assets/cloud.svg';
|
||||||
import CloudExclamationMarkIcon from 'assets/cloud-exclamation-mark.svg';
|
import CloudExclamationMarkIcon from 'assets/cloud-exclamation-mark.svg';
|
||||||
@@ -6,6 +6,7 @@ import InternalDriveIcon from 'assets/internal-drive.svg';
|
|||||||
import useDefaultStyles from './Colors';
|
import useDefaultStyles from './Colors';
|
||||||
import { EntityId } from '@reduxjs/toolkit';
|
import { EntityId } from '@reduxjs/toolkit';
|
||||||
import Svg, { Circle } from 'react-native-svg';
|
import Svg, { Circle } from 'react-native-svg';
|
||||||
|
import { Animated, Easing } from 'react-native';
|
||||||
|
|
||||||
interface DownloadIconProps {
|
interface DownloadIconProps {
|
||||||
trackId: EntityId;
|
trackId: EntityId;
|
||||||
@@ -14,10 +15,40 @@ interface DownloadIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
||||||
|
// determine styles
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
const entity = useTypedSelector((state) => state.downloads.entities[trackId]);
|
|
||||||
const iconFill = fill || defaultStyles.textHalfOpacity.color;
|
const iconFill = fill || defaultStyles.textHalfOpacity.color;
|
||||||
|
|
||||||
|
// Get download icon from state
|
||||||
|
const entity = useTypedSelector((state) => state.downloads.entities[trackId]);
|
||||||
|
|
||||||
|
// Memoize calculations for radius and circumference of the circle
|
||||||
|
const radius = useMemo(() => size / 2, [size]);
|
||||||
|
const circumference = useMemo(() => radius * 2 * Math.PI, [radius]);
|
||||||
|
|
||||||
|
// Initialize refs for the circle and the animated value
|
||||||
|
const circleRef = useRef<Circle>(null);
|
||||||
|
const offsetAnimation = useRef(new Animated.Value(entity?.progress || 0)).current;
|
||||||
|
|
||||||
|
// Whenever the progress changes, trigger the animation
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(offsetAnimation, {
|
||||||
|
toValue: (circumference * (1 - (entity?.progress || 0))),
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: false,
|
||||||
|
easing: Easing.ease,
|
||||||
|
}).start();
|
||||||
|
}, [entity?.progress, offsetAnimation, circumference]);
|
||||||
|
|
||||||
|
// On mount, subscribe to changes in the animation value and then
|
||||||
|
// apply them to the circle using native props
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = offsetAnimation.addListener((offset) => {
|
||||||
|
circleRef.current?.setNativeProps({ strokeDashoffset: offset.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => offsetAnimation.removeListener(subscription);
|
||||||
|
}, [offsetAnimation]);
|
||||||
|
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +56,7 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isComplete, isFailed, progress } = entity;
|
const { isComplete, isFailed } = entity;
|
||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
return (
|
return (
|
||||||
@@ -40,9 +71,6 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isComplete && !isFailed) {
|
if (!isComplete && !isFailed) {
|
||||||
const radius = size / 2;
|
|
||||||
const circumference = radius * 2 * Math.PI;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
|
<Svg width={size} height={size} transform={[{ rotate: '-90deg' }]}>
|
||||||
<Circle
|
<Circle
|
||||||
@@ -50,9 +78,10 @@ function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) {
|
|||||||
cy={radius}
|
cy={radius}
|
||||||
r={radius - 1}
|
r={radius - 1}
|
||||||
stroke={iconFill}
|
stroke={iconFill}
|
||||||
|
ref={circleRef}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
strokeDasharray={[ circumference, circumference ]}
|
strokeDasharray={[ circumference, circumference ]}
|
||||||
strokeDashoffset={circumference * (1 - progress)}
|
strokeDashoffset={circumference}
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
fill='transparent'
|
fill='transparent'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,8 +50,10 @@
|
|||||||
"download-album": "Download Album",
|
"download-album": "Download Album",
|
||||||
"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-track": "Delete Track",
|
||||||
"delete-all-tracks": "Delete All Tracks",
|
"delete-all-tracks": "Delete All Tracks",
|
||||||
"delete-album": "Delete Album",
|
"delete-album": "Delete Album",
|
||||||
"delete-playlist": "Delete Playlist",
|
"delete-playlist": "Delete Playlist",
|
||||||
"total-download-size": "Total Download Size"
|
"total-download-size": "Total Download Size",
|
||||||
|
"retry-failed-downloads": "Retry Failed Downloads"
|
||||||
}
|
}
|
||||||
@@ -50,5 +50,7 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'download-playlist'
|
| 'download-playlist'
|
||||||
| 'delete-album'
|
| 'delete-album'
|
||||||
| 'delete-playlist'
|
| 'delete-playlist'
|
||||||
|
| 'delete-track'
|
||||||
| 'total-download-size'
|
| 'total-download-size'
|
||||||
| 'no-downloads'
|
| 'no-downloads'
|
||||||
|
| 'retry-failed-downloads'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import useDefaultStyles from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Text, TouchableOpacity, View } from 'react-native';
|
import { FlatListProps, Text, TouchableOpacity, 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';
|
||||||
@@ -14,6 +14,16 @@ import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
|||||||
import Button from 'components/Button';
|
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';
|
||||||
|
|
||||||
|
const DownloadedTrack = styled.View`
|
||||||
|
flex: 1 0 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 8px 0;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 20px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
function Downloads() {
|
function Downloads() {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
@@ -27,18 +37,87 @@ function Downloads() {
|
|||||||
ids?.reduce<number>((sum, id) => sum + (entities[id]?.size || 0), 0)
|
ids?.reduce<number>((sum, id) => sum + (entities[id]?.size || 0), 0)
|
||||||
), [ids, entities]);
|
), [ids, entities]);
|
||||||
|
|
||||||
// Describe handlers
|
/**
|
||||||
|
* Handlers for actions in this components
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Delete a single downloaded track
|
||||||
const handleDelete = useCallback((id: EntityId) => {
|
const handleDelete = useCallback((id: EntityId) => {
|
||||||
dispatch(removeDownloadedTrack(id));
|
dispatch(removeDownloadedTrack(id));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
const handleDeleteAllTracks = useCallback(() => {
|
|
||||||
ids.forEach((id) => dispatch(removeDownloadedTrack(id)));
|
// Delete all downloaded tracks
|
||||||
}, [dispatch, ids]);
|
const handleDeleteAllTracks = useCallback(() => ids.forEach(handleDelete), [handleDelete, ids]);
|
||||||
|
|
||||||
|
// Retry a single failed track
|
||||||
const retryTrack = useCallback((id: EntityId) => {
|
const retryTrack = useCallback((id: EntityId) => {
|
||||||
dispatch(downloadTrack(id));
|
dispatch(downloadTrack(id));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// If no tracks have beend ownloaded, show a short message describing this
|
// Retry all failed tracks
|
||||||
|
const failedIds = useMemo(() => ids.filter((id) => !entities[id]?.isComplete), [ids, entities]);
|
||||||
|
const handleRetryFailed = useCallback(() => (
|
||||||
|
failedIds.forEach(retryTrack)
|
||||||
|
), [failedIds, retryTrack]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render section
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ListHeaderComponent = useMemo(() => (
|
||||||
|
<View style={{ marginHorizontal: 20, marginBottom: 12 }}>
|
||||||
|
<Text style={[{ textAlign: 'center', marginVertical: 6 }, defaultStyles.textHalfOpacity]}>
|
||||||
|
{t('total-download-size')}: {formatBytes(totalDownloadSize)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
icon={TrashIcon}
|
||||||
|
title={t('delete-all-tracks')}
|
||||||
|
onPress={handleDeleteAllTracks}
|
||||||
|
disabled={!ids.length}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={ArrowClockwise}
|
||||||
|
title={t('retry-failed-downloads')}
|
||||||
|
onPress={handleRetryFailed}
|
||||||
|
disabled={failedIds.length === 0}
|
||||||
|
style={{ marginTop: 4 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]);
|
||||||
|
|
||||||
|
const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => (
|
||||||
|
<DownloadedTrack style={defaultStyles.border}>
|
||||||
|
<View style={{ marginRight: 12 }}>
|
||||||
|
<DownloadIcon trackId={item} />
|
||||||
|
</View>
|
||||||
|
<View style={{ flexShrink: 1, marginRight: 8 }}>
|
||||||
|
<Text style={{ fontSize: 16, marginBottom: 4 }} numberOfLines={1}>
|
||||||
|
{tracks[item]?.Name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
|
||||||
|
{tracks[item]?.AlbumArtist} ({tracks[item]?.Album})
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
{entities[item]?.isComplete && entities[item]?.size ? (
|
||||||
|
<Text style={[defaultStyles.textHalfOpacity, { marginRight: 6, fontSize: 12 }]}>
|
||||||
|
{formatBytes(entities[item]?.size || 0)}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<TouchableOpacity onPress={() => handleDelete(item)}>
|
||||||
|
<TrashIcon height={24} width={24} fill={THEME_COLOR} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
{!entities[item]?.isComplete && (
|
||||||
|
<TouchableOpacity onPress={() => retryTrack(item)}>
|
||||||
|
<ArrowClockwise height={18} width={18} fill={THEME_COLOR} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</DownloadedTrack>
|
||||||
|
), [entities, retryTrack, handleDelete, defaultStyles, tracks]);
|
||||||
|
|
||||||
|
// If no tracks have been downloaded, show a short message describing this
|
||||||
if (!ids.length) {
|
if (!ids.length) {
|
||||||
return (
|
return (
|
||||||
<View style={{ margin: 24, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<View style={{ margin: 24, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
@@ -52,67 +131,11 @@ function Downloads() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
data={ids}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={ListHeaderComponent}
|
||||||
<Text style={[{ textAlign: 'center', marginVertical: 6 }, defaultStyles.textHalfOpacity]}>
|
renderItem={renderItem}
|
||||||
{t('total-download-size')}: {formatBytes(totalDownloadSize)}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
ListFooterComponent={
|
|
||||||
<View style={{ marginHorizontal: 20 }}>
|
|
||||||
<Button
|
|
||||||
icon={TrashIcon}
|
|
||||||
title={t('delete-all-tracks')}
|
|
||||||
onPress={handleDeleteAllTracks}
|
|
||||||
disabled={!ids.length}
|
|
||||||
style={{ marginTop: 8 }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
data={ids}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingVertical: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
marginHorizontal: 20,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
},
|
|
||||||
defaultStyles.border,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={{ marginRight: 12 }}>
|
|
||||||
<DownloadIcon trackId={item} />
|
|
||||||
</View>
|
|
||||||
<View style={{ flexShrink: 1, marginRight: 8 }}>
|
|
||||||
<Text style={{ fontSize: 16, marginBottom: 4 }} numberOfLines={1}>
|
|
||||||
{tracks[item]?.Name}
|
|
||||||
</Text>
|
|
||||||
<Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
|
|
||||||
{tracks[item]?.AlbumArtist} ({tracks[item]?.Album})
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
|
|
||||||
{entities[item]?.isComplete && entities[item]?.size ? (
|
|
||||||
<Text style={[defaultStyles.textHalfOpacity, { marginRight: 6, fontSize: 12 }]}>
|
|
||||||
{formatBytes(entities[item]?.size || 0)}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<TouchableOpacity onPress={() => handleDelete(item)}>
|
|
||||||
<TrashIcon height={24} width={24} fill={THEME_COLOR} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
{!entities[item]?.isComplete && (
|
|
||||||
<TouchableOpacity onPress={() => retryTrack(item)}>
|
|
||||||
<ArrowClockwise height={18} width={18} fill={THEME_COLOR} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { t } from '@localisation';
|
|||||||
import PlayIcon from 'assets/play.svg';
|
import PlayIcon from 'assets/play.svg';
|
||||||
import DownloadIcon from 'assets/cloud-down-arrow.svg';
|
import DownloadIcon from 'assets/cloud-down-arrow.svg';
|
||||||
import QueueAppendIcon from 'assets/queue-append.svg';
|
import QueueAppendIcon from 'assets/queue-append.svg';
|
||||||
|
import TrashIcon from 'assets/trash.svg';
|
||||||
import Text from 'components/Text';
|
import Text from 'components/Text';
|
||||||
import { WrappableButton, WrappableButtonRow } from 'components/WrappableButtonRow';
|
import { WrappableButton, WrappableButtonRow } from 'components/WrappableButtonRow';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { downloadTrack } from 'store/downloads/actions';
|
import { downloadTrack, removeDownloadedTrack } from 'store/downloads/actions';
|
||||||
import usePlayTracks from 'utility/usePlayTracks';
|
import usePlayTracks from 'utility/usePlayTracks';
|
||||||
|
import { selectIsDownloaded } from 'store/downloads/selectors';
|
||||||
|
|
||||||
type Route = RouteProp<ModalStackParams, 'TrackPopupMenu'>;
|
type Route = RouteProp<ModalStackParams, 'TrackPopupMenu'>;
|
||||||
|
|
||||||
@@ -24,30 +26,46 @@ const Container = styled.View`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function TrackPopupMenu() {
|
function TrackPopupMenu() {
|
||||||
// Retrieve helpers
|
// Retrieve trackId from route
|
||||||
const { params: { trackId } } = useRoute<Route>();
|
const { params: { trackId } } = useRoute<Route>();
|
||||||
|
|
||||||
|
// Retrieve helpers
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const track = useTypedSelector((state) => state.music.tracks.entities[trackId]);
|
|
||||||
const playTracks = usePlayTracks();
|
const playTracks = usePlayTracks();
|
||||||
|
|
||||||
|
// Retrieve data from store
|
||||||
|
const track = useTypedSelector((state) => state.music.tracks.entities[trackId]);
|
||||||
|
const isDownloaded = useTypedSelector(selectIsDownloaded(trackId));
|
||||||
|
|
||||||
// Set callback to close the modal
|
// Set callback to close the modal
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
navigation.dispatch(StackActions.popToTop());
|
navigation.dispatch(StackActions.popToTop());
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
// Callback for adding the track to the queue as the next song
|
||||||
const handlePlayNext = useCallback(() => {
|
const handlePlayNext = useCallback(() => {
|
||||||
playTracks([trackId], { method: 'add-after-currently-playing', play: false });
|
playTracks([trackId], { method: 'add-after-currently-playing', play: false });
|
||||||
closeModal();
|
closeModal();
|
||||||
}, [playTracks, closeModal, trackId]);
|
}, [playTracks, closeModal, trackId]);
|
||||||
|
|
||||||
|
// Callback for adding the track to the end of the queue
|
||||||
const handleAddToQueue = useCallback(() => {
|
const handleAddToQueue = useCallback(() => {
|
||||||
playTracks([trackId], { method: 'add-to-end', play: false });
|
playTracks([trackId], { method: 'add-to-end', play: false });
|
||||||
closeModal();
|
closeModal();
|
||||||
}, [playTracks, closeModal, trackId]);
|
}, [playTracks, closeModal, trackId]);
|
||||||
|
|
||||||
|
// Callback for downloading the track
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
dispatch(downloadTrack(trackId));
|
dispatch(downloadTrack(trackId));
|
||||||
closeModal();
|
closeModal();
|
||||||
}, [trackId, dispatch]);
|
}, [trackId, dispatch, closeModal]);
|
||||||
|
|
||||||
|
// Callback for removing the downloaded track
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
dispatch(removeDownloadedTrack(trackId));
|
||||||
|
closeModal();
|
||||||
|
}, [trackId, dispatch, closeModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal fullSize={false}>
|
<Modal fullSize={false}>
|
||||||
@@ -57,7 +75,11 @@ function TrackPopupMenu() {
|
|||||||
<WrappableButtonRow>
|
<WrappableButtonRow>
|
||||||
<WrappableButton title={t('play-next')} icon={PlayIcon} onPress={handlePlayNext} />
|
<WrappableButton title={t('play-next')} icon={PlayIcon} onPress={handlePlayNext} />
|
||||||
<WrappableButton title={t('add-to-queue')} icon={QueueAppendIcon} onPress={handleAddToQueue} />
|
<WrappableButton title={t('add-to-queue')} icon={QueueAppendIcon} onPress={handleAddToQueue} />
|
||||||
<WrappableButton title={t('download-track')} icon={DownloadIcon} onPress={handleDownload} />
|
{isDownloaded ? (
|
||||||
|
<WrappableButton title={t('delete-track')} icon={TrashIcon} onPress={handleDelete} />
|
||||||
|
) : (
|
||||||
|
<WrappableButton title={t('download-track')} icon={DownloadIcon} onPress={handleDownload} />
|
||||||
|
)}
|
||||||
</WrappableButtonRow>
|
</WrappableButtonRow>
|
||||||
</Container>
|
</Container>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const downloadTrack = createAsyncThunk(
|
|||||||
// Actually kick off the download
|
// Actually kick off the download
|
||||||
const { promise } = await downloadFile({
|
const { promise } = await downloadFile({
|
||||||
fromUrl: url,
|
fromUrl: url,
|
||||||
progressInterval: 50,
|
progressInterval: 250,
|
||||||
background: true,
|
background: true,
|
||||||
begin: ({ jobId, contentLength }) => {
|
begin: ({ jobId, contentLength }) => {
|
||||||
// Dispatch the initialization
|
// Dispatch the initialization
|
||||||
|
|||||||
@@ -2,12 +2,28 @@ import { createSelector, EntityId } from '@reduxjs/toolkit';
|
|||||||
import { intersection } from 'lodash';
|
import { intersection } from 'lodash';
|
||||||
import { AppState } from 'store';
|
import { AppState } from 'store';
|
||||||
|
|
||||||
|
export const selectAllDownloads = (state: AppState) => state.downloads;
|
||||||
|
export const selectDownloadedEntities = (state: AppState) => state.downloads.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only retain the supplied trackIds that have successfully been downloaded
|
||||||
|
*/
|
||||||
export const selectDownloadedTracks = (trackIds: EntityId[]) => (
|
export const selectDownloadedTracks = (trackIds: EntityId[]) => (
|
||||||
createSelector(
|
createSelector(
|
||||||
(state: AppState) => state.downloads,
|
selectAllDownloads,
|
||||||
({ entities, ids }) => {
|
({ entities, ids }) => {
|
||||||
return intersection(trackIds, ids)
|
return intersection(trackIds, ids)
|
||||||
.filter((id) => entities[id]?.isComplete);
|
.filter((id) => entities[id]?.isComplete);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a boolean that indicates whether the track is downloaded
|
||||||
|
*/
|
||||||
|
export const selectIsDownloaded = (trackId: string) => (
|
||||||
|
createSelector(
|
||||||
|
selectDownloadedEntities,
|
||||||
|
(entities) => entities[trackId]?.isComplete,
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user