diff --git a/packages/common/src/hooks/useProxySelector.ts b/packages/common/src/hooks/useProxySelector.ts index a383eefff0..6ededed33e 100644 --- a/packages/common/src/hooks/useProxySelector.ts +++ b/packages/common/src/hooks/useProxySelector.ts @@ -6,7 +6,7 @@ import { useSelector } from 'react-redux' import type { CommonState } from '../store/commonStore' -const createProxySelectorHook = () => { +export const createProxySelectorHook = () => { const useProxySelector = ( fn: (state: TState) => TReturnType, deps: DependencyList diff --git a/packages/mobile/src/components/core/Switch.tsx b/packages/mobile/src/components/core/Switch.tsx index f74c5075ed..d35e60d9f9 100644 --- a/packages/mobile/src/components/core/Switch.tsx +++ b/packages/mobile/src/components/core/Switch.tsx @@ -20,6 +20,7 @@ export const Switch = (props: SwitchProps) => { defaultValue = false, value, onValueChange: onValueChangeProp, + style: styleProp, ...other } = props const { neutralLight6, white, secondary } = useThemeColors() @@ -38,7 +39,7 @@ export const Switch = (props: SwitchProps) => { return ( { const { data } = useDrawer(drawerName) - const { collectionId, tracksForDownload } = data + const { collectionId } = data - const isAlbum = useSelector( - (state) => state.collections.entries[collectionId]?.metadata.is_album - ) + const collection = useSelector((state) => + getCollection(state, { id: collectionId }) + ) as Collection + + const { is_album, tracks } = collection + + const tracksForDownload = tracks?.map(({ track_id }) => ({ + trackId: track_id, + downloadReason: { + is_from_favorites: false, + collection_id: collectionId.toString() + } + })) const handleConfirm = useCallback(() => { - removeCollectionDownload(collectionId, tracksForDownload) + removeCollectionDownload(collectionId.toString(), tracksForDownload ?? []) }, [collectionId, tracksForDownload]) return ( ) diff --git a/packages/mobile/src/components/offline-downloads/DownloadStatusIndicator.tsx b/packages/mobile/src/components/offline-downloads/DownloadStatusIndicator.tsx index 3601c4674e..e3c14c284f 100644 --- a/packages/mobile/src/components/offline-downloads/DownloadStatusIndicator.tsx +++ b/packages/mobile/src/components/offline-downloads/DownloadStatusIndicator.tsx @@ -1,14 +1,8 @@ import { useMemo } from 'react' import { cacheCollectionsSelectors } from '@audius/common' -import { View } from 'react-native' import { useSelector } from 'react-redux' -import Rive from 'rive-react-native' -import IconDownloadFailed from 'app/assets/images/iconDownloadFailed.svg' -import IconDownloadInactive from 'app/assets/images/iconDownloadInactive.svg' -import IconDownloadQueued from 'app/assets/images/iconDownloadQueued.svg' -import IconDownloaded from 'app/assets/images/iconDownloaded.svg' import { useIsOfflineModeEnabled } from 'app/hooks/useIsOfflineModeEnabled' import { getIsCollectionMarkedForDownload, @@ -16,44 +10,23 @@ import { getTrackOfflineDownloadStatus, getIsAllDownloadsErrored } from 'app/store/offline-downloads/selectors' -import { OfflineTrackDownloadStatus } from 'app/store/offline-downloads/slice' -import { makeStyles } from 'app/styles' -import { useThemeVariant } from 'app/utils/theme' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' + +import { DownloadStatusIndicator as DownloadStatusIndicatorBase } from './DownloadStatusIndicatorBase' const { getCollection } = cacheCollectionsSelectors type TrackDownloadIndicatorProps = { trackId?: number collectionId?: string - statusOverride?: OfflineTrackDownloadStatus | null - showNotDownloaded?: boolean size?: number } -const useStyles = makeStyles(({ palette }) => ({ - iconDownloadQueued: { - fill: palette.neutralLight4 - }, - iconDownloaded: { - fill: palette.secondary - }, - iconDownloadFailed: { - fill: palette.secondary - }, - iconDownloadInactive: { - fill: palette.neutralLight4 - } -})) - export const DownloadStatusIndicator = ({ collectionId, trackId, - statusOverride, - showNotDownloaded, size = 24 }: TrackDownloadIndicatorProps) => { - const styles = useStyles() - const themeVariant = useThemeVariant() const isOfflineModeEnabled = useIsOfflineModeEnabled() const isMarkedForDownload = useSelector( getIsCollectionMarkedForDownload(collectionId) @@ -86,64 +59,16 @@ export const DownloadStatusIndicator = ({ ) const downloadStatus = - statusOverride ?? trackDownloadStatus ?? (isMarkedForDownload ? isAnyDownloadInProgress - ? OfflineTrackDownloadStatus.LOADING + ? OfflineDownloadStatus.LOADING : isAllDownloadsErrored - ? OfflineTrackDownloadStatus.ERROR - : OfflineTrackDownloadStatus.SUCCESS + ? OfflineDownloadStatus.ERROR + : OfflineDownloadStatus.SUCCESS : null) if (!isOfflineModeEnabled) return null - switch (downloadStatus) { - case OfflineTrackDownloadStatus.INIT: - return ( - - ) - case OfflineTrackDownloadStatus.LOADING: - return ( - - - - ) - case OfflineTrackDownloadStatus.SUCCESS: - return ( - - ) - case OfflineTrackDownloadStatus.ERROR: - // TODO: clickable to retry - return ( - - ) - default: - return showNotDownloaded ? ( - - ) : null - } + return } diff --git a/packages/mobile/src/components/offline-downloads/DownloadStatusIndicatorBase.tsx b/packages/mobile/src/components/offline-downloads/DownloadStatusIndicatorBase.tsx new file mode 100644 index 0000000000..7e8c4f2c25 --- /dev/null +++ b/packages/mobile/src/components/offline-downloads/DownloadStatusIndicatorBase.tsx @@ -0,0 +1,91 @@ +import type { Nullable } from '@audius/common' +import type { StyleProp, ViewStyle } from 'react-native' +import { View } from 'react-native' +import Rive from 'rive-react-native' + +import IconDownloadFailed from 'app/assets/images/iconDownloadFailed.svg' +import IconDownloadInactive from 'app/assets/images/iconDownloadInactive.svg' +import IconDownloadQueued from 'app/assets/images/iconDownloadQueued.svg' +import IconDownloaded from 'app/assets/images/iconDownloaded.svg' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' +import { makeStyles } from 'app/styles' +import { useThemeVariant } from 'app/utils/theme' + +type DownloadStatusIndicatorProps = { + status: Nullable + size?: number + style?: StyleProp +} + +const useStyles = makeStyles(({ palette }) => ({ + iconDownloadQueued: { + fill: palette.neutralLight4 + }, + iconDownloaded: { + fill: palette.secondary + }, + iconDownloadFailed: { + fill: palette.secondary + }, + iconDownloadInactive: { + fill: palette.neutralLight4 + } +})) + +export const DownloadStatusIndicator = ( + props: DownloadStatusIndicatorProps +) => { + const { status, size = 24, style } = props + const styles = useStyles() + const themeVariant = useThemeVariant() + + const renderIndicator = () => { + switch (status) { + case OfflineDownloadStatus.INIT: + return ( + + ) + case OfflineDownloadStatus.LOADING: + return ( + + + + ) + case OfflineDownloadStatus.SUCCESS: + return ( + + ) + case OfflineDownloadStatus.ERROR: + return ( + + ) + case OfflineDownloadStatus.INACTIVE: + return ( + + ) + default: + return null + } + } + return {renderIndicator()} +} diff --git a/packages/mobile/src/components/offline-downloads/DownloadToggle.tsx b/packages/mobile/src/components/offline-downloads/DownloadToggle.tsx deleted file mode 100644 index f959e7a933..0000000000 --- a/packages/mobile/src/components/offline-downloads/DownloadToggle.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' - -import type { Collection, CommonState, DownloadReason } from '@audius/common' -import { reachabilitySelectors } from '@audius/common' -import { View } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' - -import { Switch, Text } from 'app/components/core' -import { getAccountCollections } from 'app/screens/favorites-screen/selectors' -import { - downloadAllFavorites, - downloadCollection, - DOWNLOAD_REASON_FAVORITES -} from 'app/services/offline-downloader' -import { setVisibility } from 'app/store/drawers/slice' -import { - getIsCollectionMarkedForDownload, - getIsAnyDownloadInProgress -} from 'app/store/offline-downloads/selectors' -import { OfflineTrackDownloadStatus as OfflineDownloadStatus } from 'app/store/offline-downloads/slice' -import { makeStyles } from 'app/styles' - -import { DownloadStatusIndicator } from './DownloadStatusIndicator' - -const { getIsReachable } = reachabilitySelectors - -export type TrackForDownload = { - trackId: number - downloadReason: DownloadReason - // Timestamp associated with when this track was favorited if the reason - // is favorites. - favoriteCreatedAt?: string -} - -type DownloadToggleProps = { - tracksForDownload: TrackForDownload[] - labelText?: string - collection?: Collection | null - isFavoritesDownload?: boolean -} - -const messages = { - downloading: 'Downloading' -} - -const useStyles = makeStyles(({ palette, spacing }) => ({ - root: { - flexDirection: 'row', - alignItems: 'center' - }, - rootWithLabel: { - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - height: spacing(8), - marginBottom: spacing(1) - }, - flex1: { - flex: 1 - }, - iconTitle: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: 'auto', - justifyContent: 'center' - }, - labelText: { - color: palette.neutralLight4, - fontSize: 14, - letterSpacing: 2, - textAlign: 'center', - textTransform: 'uppercase', - paddingLeft: spacing(1), - flexBasis: 'auto' - }, - toggleContainer: { - flex: 1, - flexDirection: 'row', - justifyContent: 'flex-end' - }, - purple: { - color: palette.secondary - } -})) - -export const DownloadToggle = ({ - tracksForDownload, - collection, - labelText, - isFavoritesDownload: isAllFavoritesToggle -}: DownloadToggleProps) => { - const styles = useStyles() - const dispatch = useDispatch() - const isReachable = useSelector(getIsReachable) - const [disabled, setDisabled] = useState(false) - const collectionIdStr = isAllFavoritesToggle - ? DOWNLOAD_REASON_FAVORITES - : collection?.playlist_id?.toString() - - const trackIds = useMemo( - () => tracksForDownload.map(({ trackId }) => trackId), - [tracksForDownload] - ) - - const isAnyDownloadInProgress = useSelector((state) => - getIsAnyDownloadInProgress(state, trackIds) - ) - - const isCollectionMarkedForDownload = useSelector( - getIsCollectionMarkedForDownload(collectionIdStr) - ) - - const userCollections = useSelector((state: CommonState) => - getAccountCollections(state, '') - ) - const isFavoritesMarkedForDownload = useSelector( - getIsCollectionMarkedForDownload(DOWNLOAD_REASON_FAVORITES) - ) - const isThisFavoritedCollectionDownload = useMemo( - () => - !!( - collection && - isFavoritesMarkedForDownload && - userCollections.some( - (userCollection) => - userCollection.playlist_id === collection.playlist_id - ) - ), - [collection, isFavoritesMarkedForDownload, userCollections] - ) - - const handleToggleDownload = useCallback( - (isDownloadEnabled: boolean) => { - if (!collection && !isAllFavoritesToggle) return - if (isDownloadEnabled) { - isAllFavoritesToggle - ? downloadAllFavorites() - : collection && - downloadCollection(collection, /* isFavoritesDownload */ false) - } else { - if (!isAllFavoritesToggle && collectionIdStr) { - // we are trying to remove download from a collection page - dispatch( - setVisibility({ - drawer: 'RemoveDownloadedCollection', - visible: true, - data: { collectionId: collectionIdStr, tracksForDownload } - }) - ) - } else if (collectionIdStr) { - dispatch( - setVisibility({ - drawer: 'RemoveDownloadedFavorites', - visible: true, - data: { collectionId: collectionIdStr, tracksForDownload } - }) - ) - } - } - setDisabled(true) - setTimeout(() => setDisabled(false), 1000) - }, - [ - collection, - collectionIdStr, - dispatch, - isAllFavoritesToggle, - tracksForDownload - ] - ) - - const switchOn = - isCollectionMarkedForDownload || isThisFavoritedCollectionDownload - - return ( - - {labelText ? : null} - - {collection || isAllFavoritesToggle ? ( - - ) : null} - {labelText ? ( - - {isAnyDownloadInProgress ? messages.downloading : labelText} - - ) : null} - - - {collection || isAllFavoritesToggle ? ( - - ) : null} - - - ) -} diff --git a/packages/mobile/src/components/offline-downloads/index.ts b/packages/mobile/src/components/offline-downloads/index.ts index f78a7d7103..7ec63ac9b2 100644 --- a/packages/mobile/src/components/offline-downloads/index.ts +++ b/packages/mobile/src/components/offline-downloads/index.ts @@ -1,2 +1,2 @@ export * from './DownloadStatusIndicator' -export * from './DownloadToggle' +export * from './types' diff --git a/packages/mobile/src/components/offline-downloads/types.ts b/packages/mobile/src/components/offline-downloads/types.ts new file mode 100644 index 0000000000..5f74446815 --- /dev/null +++ b/packages/mobile/src/components/offline-downloads/types.ts @@ -0,0 +1,9 @@ +import type { DownloadReason } from '@audius/common' + +export type TrackForDownload = { + trackId: number + downloadReason: DownloadReason + // Timestamp associated with when this track was favorited if the reason + // is favorites. + favoriteCreatedAt?: string +} diff --git a/packages/mobile/src/hooks/useProxySelector.ts b/packages/mobile/src/hooks/useProxySelector.ts new file mode 100644 index 0000000000..34eb4f2ad4 --- /dev/null +++ b/packages/mobile/src/hooks/useProxySelector.ts @@ -0,0 +1,4 @@ +import { createProxySelectorHook } from '@audius/common' + +import type { AppState } from 'app/store' +export const useProxySelector = createProxySelectorHook() diff --git a/packages/mobile/src/screens/collection-screen/CollectionHeader.tsx b/packages/mobile/src/screens/collection-screen/CollectionHeader.tsx new file mode 100644 index 0000000000..bb3c5ca2c6 --- /dev/null +++ b/packages/mobile/src/screens/collection-screen/CollectionHeader.tsx @@ -0,0 +1,208 @@ +import { useCallback } from 'react' + +import type { Collection, SmartCollectionVariant } from '@audius/common' +import { + reachabilitySelectors, + collectionPageSelectors, + Variant +} from '@audius/common' +import { View } from 'react-native' +import { useSelector, useDispatch } from 'react-redux' + +import { Switch, Text } from 'app/components/core' +import { DownloadStatusIndicator } from 'app/components/offline-downloads/DownloadStatusIndicatorBase' +import { useIsOfflineModeEnabled } from 'app/hooks/useIsOfflineModeEnabled' +import { useProxySelector } from 'app/hooks/useProxySelector' +import { downloadCollection } from 'app/services/offline-downloader' +import { setVisibility } from 'app/store/drawers/slice' +import { getOfflineDownloadStatus } from 'app/store/offline-downloads/selectors' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' +import { makeStyles } from 'app/styles' +const { getCollection } = collectionPageSelectors +const { getIsReachable } = reachabilitySelectors + +const messages = { + album: 'Album', + playlist: 'Playlist', + empty: 'This playlist is empty.', + privatePlaylist: 'Private Playlist', + publishing: 'Publishing...', + downloading: 'Downloading' +} + +const useStyles = makeStyles(({ spacing }) => ({ + root: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginVertical: spacing(1) + }, + headerLeft: { + flex: 1 + }, + headerCenter: { + flexDirection: 'row', + alignItems: 'center', + flexBasis: 'auto' + }, + downloadStatusIndicator: { + marginRight: spacing(2) + }, + headerRight: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end' + }, + headerText: { + letterSpacing: 2, + textAlign: 'center', + textTransform: 'uppercase' + }, + rootLegacy: { + marginVertical: spacing(2) + } +})) + +type CollectionHeaderProps = { + collectionId?: number | SmartCollectionVariant +} + +export const CollectionHeader = (props: CollectionHeaderProps) => { + const { collectionId } = props + const collection = useSelector((state) => + getCollection( + state, + typeof collectionId === 'number' ? { id: collectionId } : undefined + ) + ) + const isOfflineModeEnabled = useIsOfflineModeEnabled() + const styles = useStyles() + + const getHeaderText = () => { + if (!collection) return messages.playlist + if (collection.variant === Variant.SMART) return messages.playlist + const { _is_publishing, is_album, is_private } = collection + + if (_is_publishing) return messages.publishing + if (is_album) return messages.album + if (is_private) return messages.privatePlaylist + return messages.playlist + } + + if (isOfflineModeEnabled && collection?.variant === Variant.USER_GENERATED) { + return ( + + ) + } + + return ( + + + {getHeaderText()} + + + ) +} + +type OfflineCollectionHeaderProps = { + collection: Collection + headerText: string +} + +const OfflineCollectionHeader = (props: OfflineCollectionHeaderProps) => { + const styles = useStyles() + const { collection, headerText } = props + const { playlist_id, playlist_contents } = collection + const { track_ids } = playlist_contents + const dispatch = useDispatch() + const isReachable = useSelector(getIsReachable) + + const isMarkedForDownload = useProxySelector( + (state) => { + const { collections, favoritedCollections } = state.offlineDownloads + return collections[playlist_id] || favoritedCollections[playlist_id] + }, + [playlist_id] + ) + + const isDownloaded = useProxySelector( + (state) => { + const trackIds = track_ids.map(({ track }) => track) + const downloadStatus = getOfflineDownloadStatus(state) + return trackIds.every( + (trackId) => downloadStatus[trackId] === OfflineDownloadStatus.SUCCESS + ) + }, + [track_ids] + ) + + const getDownloadStatus = () => { + if (!isMarkedForDownload) { + return OfflineDownloadStatus.INACTIVE + } + if (isDownloaded) { + return OfflineDownloadStatus.SUCCESS + } + return OfflineDownloadStatus.LOADING + } + + const downloadStatus = getDownloadStatus() + + const handleToggleDownload = useCallback( + (isDownloadEnabled: boolean) => { + if (isDownloadEnabled) { + downloadCollection(collection) + } else { + dispatch( + setVisibility({ + drawer: 'RemoveDownloadedCollection', + visible: true, + data: { collectionId: playlist_id } + }) + ) + } + }, + [collection, dispatch, playlist_id] + ) + + return ( + + + + + + {downloadStatus === OfflineDownloadStatus.LOADING + ? messages.downloading + : headerText} + + + + + + + ) +} diff --git a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx index dfd1c24c3c..fb55db20b8 100644 --- a/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/collection-screen/CollectionScreenDetailsTile.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react' import type { ID, Maybe, SmartCollectionVariant, UID } from '@audius/common' import { - Variant, useProxySelector, collectionPageActions, playerSelectors, @@ -25,19 +24,16 @@ import type { DetailsTileDetail, DetailsTileProps } from 'app/components/details-tile/types' -import { DownloadToggle } from 'app/components/offline-downloads' import { TrackList } from 'app/components/track-list' import { useIsOfflineModeEnabled } from 'app/hooks/useIsOfflineModeEnabled' import { useOfflineCollectionLineup } from 'app/hooks/useLoadOfflineTracks' import { make, track } from 'app/services/analytics' import { makeStyles } from 'app/styles' import { formatCount } from 'app/utils/format' -const { - getCollection, - getCollectionTracksLineup, - getCollectionUid, - getUserUid -} = collectionPageSelectors + +import { CollectionHeader } from './CollectionHeader' +const { getCollectionTracksLineup, getCollectionUid, getUserUid } = + collectionPageSelectors const { resetCollection } = collectionPageActions const { makeGetTableMetadatas } = lineupSelectors const { getPlaying, getUid, getCurrentTrack } = playerSelectors @@ -105,7 +101,6 @@ export const CollectionScreenDetailsTile = ({ const isOfflineModeEnabled = useIsOfflineModeEnabled() const isReachable = useSelector(getIsReachable) - const collection = useSelector(getCollection) const collectionUid = useSelector(getCollectionUid) const userUid = useSelector(getUserUid) const { entries, status } = useProxySelector(getTracksLineup, [isReachable]) @@ -218,28 +213,11 @@ export const CollectionScreenDetailsTile = ({ return messages.playlist }, [isAlbum, isPrivate, isPublishing]) - const tracksForDownload = useMemo( - () => - entries.map((track) => ({ - trackId: track.track_id, - downloadReason: { - is_from_favorites: false, - collection_id: collectionId?.toString() - } - })), - [collectionId, entries] + const renderHeader = useCallback( + () => , + [collectionId] ) - const renderHeader = useCallback(() => { - return collection?.variant === Variant.SMART ? null : ( - - ) - }, [collection, headerText, tracksForDownload]) - const renderTrackList = () => { if (tracksLoading) return ( diff --git a/packages/mobile/src/screens/favorites-screen/AlbumsTab.tsx b/packages/mobile/src/screens/favorites-screen/AlbumsTab.tsx index 35992a4ba4..722fd4fd8a 100644 --- a/packages/mobile/src/screens/favorites-screen/AlbumsTab.tsx +++ b/packages/mobile/src/screens/favorites-screen/AlbumsTab.tsx @@ -12,7 +12,7 @@ import { getOfflineDownloadStatus, getIsDoneLoadingFromDisk } from 'app/store/offline-downloads/selectors' -import { OfflineTrackDownloadStatus } from 'app/store/offline-downloads/slice' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' import { FilterInput } from './FilterInput' import { NoTracksPlaceholder } from './NoTracksPlaceholder' @@ -56,7 +56,7 @@ export const AlbumsTab = () => { trackIds.some((t) => { return ( offlineDownloadStatus[t.toString()] === - OfflineTrackDownloadStatus.SUCCESS + OfflineDownloadStatus.SUCCESS ) })) ) diff --git a/packages/mobile/src/screens/favorites-screen/DownloadFavoritesSwitch.tsx b/packages/mobile/src/screens/favorites-screen/DownloadFavoritesSwitch.tsx new file mode 100644 index 0000000000..a92286c0ea --- /dev/null +++ b/packages/mobile/src/screens/favorites-screen/DownloadFavoritesSwitch.tsx @@ -0,0 +1,108 @@ +import { useCallback } from 'react' + +import { reachabilitySelectors } from '@audius/common' +import { View } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' + +import { Switch } from 'app/components/core' +import { DownloadStatusIndicator } from 'app/components/offline-downloads/DownloadStatusIndicatorBase' +import { useProxySelector } from 'app/hooks/useProxySelector' +import { + downloadAllFavorites, + DOWNLOAD_REASON_FAVORITES +} from 'app/services/offline-downloader' +import { setVisibility } from 'app/store/drawers/slice' +import { getOfflineDownloadStatus } from 'app/store/offline-downloads/selectors' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' +import { makeStyles } from 'app/styles' +const { getIsReachable } = reachabilitySelectors + +const useStyles = makeStyles(({ spacing }) => ({ + root: { + flexDirection: 'row', + alignItems: 'center' + }, + downloadStatusIndicator: { + marginRight: spacing(1) + } +})) + +export const DownloadFavoritesSwitch = () => { + const styles = useStyles() + const dispatch = useDispatch() + const isReachable = useSelector(getIsReachable) + + const isMarkedForDownload = useProxySelector((state) => { + const { collections, favoritedCollections } = state.offlineDownloads + return ( + collections[DOWNLOAD_REASON_FAVORITES] || + favoritedCollections[DOWNLOAD_REASON_FAVORITES] + ) + }, []) + + const isDownloaded = useProxySelector((state) => { + const downloadStatus = getOfflineDownloadStatus(state) + const tracksToDownload = Object.keys(downloadStatus) + if (tracksToDownload.length === 0) return false + + const hasRemainingDownloads = tracksToDownload.some( + (trackId) => + downloadStatus[trackId] === OfflineDownloadStatus.LOADING || + downloadStatus[trackId] === OfflineDownloadStatus.INIT + ) + + if (hasRemainingDownloads) return false + + // Since downloadStatus can take a while to populate, use favorited + // tracks lineup as a backup + const favoritedTracks = state.pages.savedPage.tracks.entries + return favoritedTracks.every(({ id }) => { + return ( + downloadStatus[id] === OfflineDownloadStatus.SUCCESS || + downloadStatus[id] === OfflineDownloadStatus.ERROR + ) + }) + }, []) + + const getDownloadStatus = () => { + if (!isMarkedForDownload) { + return OfflineDownloadStatus.INACTIVE + } + if (isDownloaded) { + return OfflineDownloadStatus.SUCCESS + } + return OfflineDownloadStatus.LOADING + } + + const downloadStatus = getDownloadStatus() + + const handleToggleDownload = useCallback( + (isDownloadEnabled: boolean) => { + if (isDownloadEnabled) { + downloadAllFavorites() + } else { + dispatch( + setVisibility({ + drawer: 'RemoveDownloadedFavorites', + visible: true + }) + ) + } + }, + [dispatch] + ) + + return ( + + + + + ) +} diff --git a/packages/mobile/src/components/offline-downloads/DownloadProgress.tsx b/packages/mobile/src/screens/favorites-screen/DownloadProgress.tsx similarity index 88% rename from packages/mobile/src/components/offline-downloads/DownloadProgress.tsx rename to packages/mobile/src/screens/favorites-screen/DownloadProgress.tsx index 43fb9fc919..ecba87fd9a 100644 --- a/packages/mobile/src/components/offline-downloads/DownloadProgress.tsx +++ b/packages/mobile/src/screens/favorites-screen/DownloadProgress.tsx @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux' import { Text } from 'app/components/core' import { ProgressBar } from 'app/components/progress-bar' import { getOfflineDownloadStatus } from 'app/store/offline-downloads/selectors' -import { OfflineTrackDownloadStatus } from 'app/store/offline-downloads/slice' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' import { makeStyles } from 'app/styles' const useStyles = makeStyles(({ palette, spacing }) => ({ @@ -32,8 +32,8 @@ export const DownloadProgress = () => { const numDownloads = Object.keys(downloadStatus).length const numDownloadsComplete = Object.values(downloadStatus).filter( (status) => - status === OfflineTrackDownloadStatus.SUCCESS || - status === OfflineTrackDownloadStatus.ERROR + status === OfflineDownloadStatus.SUCCESS || + status === OfflineDownloadStatus.ERROR ).length // Only render if there are active downloads diff --git a/packages/mobile/src/screens/favorites-screen/FavoritesScreen.tsx b/packages/mobile/src/screens/favorites-screen/FavoritesScreen.tsx index 8720b5db8f..29408e0065 100644 --- a/packages/mobile/src/screens/favorites-screen/FavoritesScreen.tsx +++ b/packages/mobile/src/screens/favorites-screen/FavoritesScreen.tsx @@ -1,13 +1,6 @@ -import { useMemo } from 'react' - -import type { CommonState } from '@audius/common' -import { - accountActions, - useProxySelector, - reachabilitySelectors -} from '@audius/common' +import { accountActions } from '@audius/common' import { View } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { useEffectOnce } from 'react-use' import IconAlbum from 'app/assets/images/iconAlbum.svg' @@ -15,25 +8,19 @@ import IconFavorite from 'app/assets/images/iconFavorite.svg' import IconNote from 'app/assets/images/iconNote.svg' import IconPlaylists from 'app/assets/images/iconPlaylists.svg' import { Screen, ScreenContent, ScreenHeader } from 'app/components/core' -import type { TrackForDownload } from 'app/components/offline-downloads' -import { DownloadToggle } from 'app/components/offline-downloads' -import { DownloadProgress } from 'app/components/offline-downloads/DownloadProgress' import { TopTabNavigator } from 'app/components/top-tab-bar' import { useAppTabScreen } from 'app/hooks/useAppTabScreen' -import { useFetchAllFavoritedTracks } from 'app/hooks/useFetchAllFavoritedTracks' import { useIsOfflineModeEnabled, useReadOfflineOverride } from 'app/hooks/useIsOfflineModeEnabled' -import { DOWNLOAD_REASON_FAVORITES } from 'app/services/offline-downloader' -import { getIsDoneLoadingFromDisk } from 'app/store/offline-downloads/selectors' import { AlbumsTab } from './AlbumsTab' +import { DownloadFavoritesSwitch } from './DownloadFavoritesSwitch' +import { DownloadProgress } from './DownloadProgress' import { PlaylistsTab } from './PlaylistsTab' import { TracksTab } from './TracksTab' -import { getAccountCollections } from './selectors' const { fetchSavedPlaylists, fetchSavedAlbums } = accountActions -const { getIsReachable } = reachabilitySelectors const messages = { header: 'Favorites' @@ -61,9 +48,6 @@ export const FavoritesScreen = () => { useAppTabScreen() const dispatch = useDispatch() const isOfflineModeEnabled = useIsOfflineModeEnabled() - const isDoneLoadingFromDisk = useSelector(getIsDoneLoadingFromDisk) - const isReachable = useSelector(getIsReachable) - const { value: allFavoritedTrackIds } = useFetchAllFavoritedTracks() useReadOfflineOverride() @@ -72,44 +56,6 @@ export const FavoritesScreen = () => { dispatch(fetchSavedAlbums()) }) - const userCollections = useProxySelector( - (state: CommonState) => { - if (isOfflineModeEnabled && !isReachable) { - if (!isDoneLoadingFromDisk) { - return [] - } - } - return getAccountCollections(state, '') - }, - [isOfflineModeEnabled, isReachable, isDoneLoadingFromDisk] - ) - - const tracksForDownload: TrackForDownload[] = useMemo(() => { - const trackFavoritesToDownload: TrackForDownload[] = ( - allFavoritedTrackIds ?? [] - ).map(({ trackId, favoriteCreatedAt }) => ({ - trackId, - downloadReason: { - is_from_favorites: true, - collection_id: DOWNLOAD_REASON_FAVORITES, - favorite_created_at: favoriteCreatedAt - } - })) - const collectionFavoritesToDownload: TrackForDownload[] = - userCollections.flatMap((collection) => - collection.playlist_contents.track_ids.map(({ track: trackId }) => ({ - trackId, - downloadReason: { - is_from_favorites: true, - collection_id: collection.playlist_id.toString() - } - // TODO: include a favorite_created_at timestamp for sorting offline collections - })) - ) - - return trackFavoritesToDownload.concat(collectionFavoritesToDownload) - }, [allFavoritedTrackIds, userCollections]) - return ( { icon={IconFavorite} styles={{ icon: { marginLeft: 3 } }} > - {isOfflineModeEnabled && ( + {isOfflineModeEnabled ? ( - + - )} + ) : null} {} diff --git a/packages/mobile/src/screens/favorites-screen/PlaylistsTab.tsx b/packages/mobile/src/screens/favorites-screen/PlaylistsTab.tsx index 159447e65f..b3bcd60c36 100644 --- a/packages/mobile/src/screens/favorites-screen/PlaylistsTab.tsx +++ b/packages/mobile/src/screens/favorites-screen/PlaylistsTab.tsx @@ -13,7 +13,7 @@ import { getIsDoneLoadingFromDisk, getOfflineDownloadStatus } from 'app/store/offline-downloads/selectors' -import { OfflineTrackDownloadStatus } from 'app/store/offline-downloads/slice' +import { OfflineDownloadStatus } from 'app/store/offline-downloads/slice' import type { FavoritesTabScreenParamList } from '../app-screen/FavoritesTabScreen' @@ -61,7 +61,7 @@ export const PlaylistsTab = () => { trackIds.some((t) => { return ( offlineDownloadStatus[t.toString()] === - OfflineTrackDownloadStatus.SUCCESS + OfflineDownloadStatus.SUCCESS ) })) ) diff --git a/packages/mobile/src/screens/favorites-screen/selectors.ts b/packages/mobile/src/screens/favorites-screen/selectors.ts index 9fd00ed075..d928bbe544 100644 --- a/packages/mobile/src/screens/favorites-screen/selectors.ts +++ b/packages/mobile/src/screens/favorites-screen/selectors.ts @@ -22,10 +22,7 @@ const matchesFilter = ( ) } -export const getAccountCollections = ( - state: CommonState, - filterValue: string -) => { +export const getAccountCollections = (state: CommonState, filterValue = '') => { const collectionEntries = state.collections?.entries const { collections } = state.account if (!collectionEntries) return [] diff --git a/packages/mobile/src/store/drawers/slice.ts b/packages/mobile/src/store/drawers/slice.ts index 385aba4b38..4a440e89fb 100644 --- a/packages/mobile/src/store/drawers/slice.ts +++ b/packages/mobile/src/store/drawers/slice.ts @@ -1,9 +1,7 @@ -import type { Nullable } from '@audius/common' +import type { ID, Nullable } from '@audius/common' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' -import type { TrackForDownload } from 'app/components/offline-downloads' - export type BaseDrawerData = Record export type Drawer = @@ -37,13 +35,9 @@ export type DrawerData = { ConfirmRemoveWallet: undefined ShareToStoryProgress: undefined UnfavoriteDownloadedCollection: { collectionId: number } - RemoveDownloadedFavorites: { - collectionId: string - tracksForDownload: TrackForDownload[] - } + RemoveDownloadedFavorites: undefined RemoveDownloadedCollection: { - collectionId: string - tracksForDownload: TrackForDownload[] + collectionId: ID } } diff --git a/packages/mobile/src/store/offline-downloads/selectors.ts b/packages/mobile/src/store/offline-downloads/selectors.ts index 59615fea00..0b803808a7 100644 --- a/packages/mobile/src/store/offline-downloads/selectors.ts +++ b/packages/mobile/src/store/offline-downloads/selectors.ts @@ -3,7 +3,7 @@ import { createSelector } from 'reselect' import type { AppState } from 'app/store' import type { OfflineDownloadsState } from './slice' -import { OfflineTrackDownloadStatus } from './slice' +import { OfflineDownloadStatus } from './slice' export const getOfflineDownloadStatus = (state: AppState) => state.offlineDownloads.downloadStatus @@ -37,15 +37,18 @@ export const getIsDoneLoadingFromDisk = (state: AppState): boolean => state.offlineDownloads.isDoneLoadingFromDisk export const getIsAnyDownloadInProgress = createSelector( - [getOfflineDownloadStatus, (state, trackIds) => trackIds], + [ + getOfflineDownloadStatus, + (_state: AppState, trackIds: number[]) => trackIds + ], (offlineDownloadStatus, trackIds) => { return trackIds.some((trackId: number) => { const status = offlineDownloadStatus[trackId.toString()] // Any download is in progress if any are loading (actively downloading) // or init (queued, but not actively downloading) return ( - status === OfflineTrackDownloadStatus.LOADING || - status === OfflineTrackDownloadStatus.INIT + status === OfflineDownloadStatus.LOADING || + status === OfflineDownloadStatus.INIT ) }) } @@ -58,7 +61,7 @@ export const getIsAllDownloadsErrored = createSelector( trackIds.length > 0 && trackIds.every((trackId: number) => { const status = offlineDownloadStatus[trackId.toString()] - return status === OfflineTrackDownloadStatus.ERROR + return status === OfflineDownloadStatus.ERROR }) ) } diff --git a/packages/mobile/src/store/offline-downloads/slice.ts b/packages/mobile/src/store/offline-downloads/slice.ts index da49a104ae..e66d9c93ef 100644 --- a/packages/mobile/src/store/offline-downloads/slice.ts +++ b/packages/mobile/src/store/offline-downloads/slice.ts @@ -6,7 +6,7 @@ type LineupTrack = Track & UserTrackMetadata export type OfflineDownloadsState = { downloadStatus: { - [key: string]: OfflineTrackDownloadStatus + [key: string]: OfflineDownloadStatus } tracks: { [key: string]: LineupTrack @@ -20,7 +20,8 @@ export type OfflineDownloadsState = { isDoneLoadingFromDisk: boolean } -export enum OfflineTrackDownloadStatus { +export enum OfflineDownloadStatus { + INACTIVE = 'INACTIVE', // download is not initiated, INIT = 'INIT', // download is queued LOADING = 'LOADING', // download is in progress SUCCESS = 'SUCCESS', // download succeeded @@ -52,18 +53,18 @@ const slice = createSlice({ { payload: trackIds }: PayloadAction ) => { trackIds.forEach((trackId) => { - state.downloadStatus[trackId] = OfflineTrackDownloadStatus.INIT + state.downloadStatus[trackId] = OfflineDownloadStatus.INIT }) }, // Actually starting the download startDownload: (state, { payload: trackId }: PayloadAction) => { - state.downloadStatus[trackId] = OfflineTrackDownloadStatus.LOADING + state.downloadStatus[trackId] = OfflineDownloadStatus.LOADING }, completeDownload: (state, { payload: trackId }: PayloadAction) => { - state.downloadStatus[trackId] = OfflineTrackDownloadStatus.SUCCESS + state.downloadStatus[trackId] = OfflineDownloadStatus.SUCCESS }, errorDownload: (state, { payload: trackId }: PayloadAction) => { - state.downloadStatus[trackId] = OfflineTrackDownloadStatus.ERROR + state.downloadStatus[trackId] = OfflineDownloadStatus.ERROR }, removeDownload: (state, { payload: trackId }: PayloadAction) => { delete state.downloadStatus[trackId] @@ -92,13 +93,13 @@ const slice = createSlice({ tracks.forEach((track) => { const trackIdStr = track.track_id.toString() state.tracks[trackIdStr] = track - state.downloadStatus[trackIdStr] = OfflineTrackDownloadStatus.SUCCESS + state.downloadStatus[trackIdStr] = OfflineDownloadStatus.SUCCESS }) }, loadTrack: (state, { payload: track }: PayloadAction) => { const trackIdStr = track.track_id.toString() state.tracks[trackIdStr] = track - state.downloadStatus[trackIdStr] = OfflineTrackDownloadStatus.SUCCESS + state.downloadStatus[trackIdStr] = OfflineDownloadStatus.SUCCESS }, unloadTrack: (state, { payload: trackId }: PayloadAction) => { delete state.tracks[trackId]