mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
library: drop the download progress indicator
This commit is contained in:
@@ -78,13 +78,6 @@ data class ListDownloadStatus(
|
|||||||
val complete: Boolean get() = total > 0 && downloaded == total
|
val complete: Boolean get() = total > 0 && downloaded == total
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DownloadProgress(
|
|
||||||
val bytes: Long,
|
|
||||||
val total: Long,
|
|
||||||
) {
|
|
||||||
val fraction: Float? get() = fractionOf(bytes, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class EntityDownloadStatus(
|
data class EntityDownloadStatus(
|
||||||
val id: String,
|
val id: String,
|
||||||
val total: Long = 0,
|
val total: Long = 0,
|
||||||
|
|||||||
@@ -396,7 +396,6 @@ private fun MainNavigation() {
|
|||||||
navigateDetail(Routes.PLAYLIST_DETAIL, "playlistId", playlist.id, Routes.playlist(playlist.id))
|
navigateDetail(Routes.PLAYLIST_DETAIL, "playlistId", playlist.id, Routes.playlist(playlist.id))
|
||||||
},
|
},
|
||||||
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||||
onOpenDownloads = { navController.navigate(Routes.DOWNLOADS) },
|
|
||||||
onOpenSearch = { navController.navigate(Routes.SEARCH) },
|
onOpenSearch = { navController.navigate(Routes.SEARCH) },
|
||||||
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
|
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
|
||||||
onViewArtist = { artistId ->
|
onViewArtist = { artistId ->
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import androidx.compose.material.icons.rounded.Album
|
|||||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||||
import androidx.compose.material.icons.rounded.CloudOff
|
import androidx.compose.material.icons.rounded.CloudOff
|
||||||
import androidx.compose.material.icons.rounded.Download
|
|
||||||
import androidx.compose.material.icons.rounded.DownloadDone
|
import androidx.compose.material.icons.rounded.DownloadDone
|
||||||
import androidx.compose.material.icons.rounded.FilterAltOff
|
import androidx.compose.material.icons.rounded.FilterAltOff
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
@@ -111,7 +110,6 @@ import com.subtracks.data.model.Artist
|
|||||||
import com.subtracks.data.model.BulkDownloadAction
|
import com.subtracks.data.model.BulkDownloadAction
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.data.model.DownloadList
|
import com.subtracks.data.model.DownloadList
|
||||||
import com.subtracks.data.model.DownloadProgress
|
|
||||||
import com.subtracks.data.model.ListDownloadStatus
|
import com.subtracks.data.model.ListDownloadStatus
|
||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.prefs.AlbumSort
|
import com.subtracks.data.prefs.AlbumSort
|
||||||
@@ -230,7 +228,6 @@ fun LibraryRoute(
|
|||||||
onArtistClick: (Artist) -> Unit,
|
onArtistClick: (Artist) -> Unit,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onOpenDownloads: () -> Unit = {},
|
|
||||||
onOpenSearch: () -> Unit = {},
|
onOpenSearch: () -> Unit = {},
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
@@ -261,7 +258,6 @@ fun LibraryRoute(
|
|||||||
val listQueryState = listTab?.let { viewModel.listQuery(it).collectAsStateWithLifecycle() }
|
val listQueryState = listTab?.let { viewModel.listQuery(it).collectAsStateWithLifecycle() }
|
||||||
val listQuery = listQueryState?.value ?: ListQuery("")
|
val listQuery = listQueryState?.value ?: ListQuery("")
|
||||||
val offline by viewModel.offline.collectAsStateWithLifecycle()
|
val offline by viewModel.offline.collectAsStateWithLifecycle()
|
||||||
val downloadProgress by viewModel.downloadProgress.collectAsStateWithLifecycle()
|
|
||||||
val playSort by viewModel.playSortAvailability.collectAsStateWithLifecycle()
|
val playSort by viewModel.playSortAvailability.collectAsStateWithLifecycle()
|
||||||
val displayQuery = if (offline) listQuery.copy(downloaded = true) else listQuery
|
val displayQuery = if (offline) listQuery.copy(downloaded = true) else listQuery
|
||||||
val searchState = listTab?.let { viewModel.search(it).collectAsStateWithLifecycle() }
|
val searchState = listTab?.let { viewModel.search(it).collectAsStateWithLifecycle() }
|
||||||
@@ -319,7 +315,6 @@ fun LibraryRoute(
|
|||||||
onPlaylistClick = onPlaylistClick,
|
onPlaylistClick = onPlaylistClick,
|
||||||
onSync = viewModel::sync,
|
onSync = viewModel::sync,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onOpenDownloads = onOpenDownloads,
|
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
onItemLongClick = {
|
onItemLongClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
@@ -329,7 +324,6 @@ fun LibraryRoute(
|
|||||||
resetKeys = resetKeys,
|
resetKeys = resetKeys,
|
||||||
offline = offline,
|
offline = offline,
|
||||||
onExitOffline = { viewModel.setOffline(false) },
|
onExitOffline = { viewModel.setOffline(false) },
|
||||||
downloadProgress = downloadProgress,
|
|
||||||
sortOptions = sortOptionsFor(selectedTab, playSort),
|
sortOptions = sortOptionsFor(selectedTab, playSort),
|
||||||
starredSupported = listTab?.supportsStarred == true,
|
starredSupported = listTab?.supportsStarred == true,
|
||||||
onSortChange = { sort -> listTab?.let { viewModel.setListQuery(it, listQuery.copy(sort = sort)) } },
|
onSortChange = { sort -> listTab?.let { viewModel.setListQuery(it, listQuery.copy(sort = sort)) } },
|
||||||
@@ -404,12 +398,10 @@ fun LibraryScreen(
|
|||||||
onSync: () -> Unit,
|
onSync: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onOpenDownloads: () -> Unit = {},
|
|
||||||
artistArt: (Artist) -> ArtistArt = { ArtistArt() },
|
artistArt: (Artist) -> ArtistArt = { ArtistArt() },
|
||||||
onOpenSearch: () -> Unit = {},
|
onOpenSearch: () -> Unit = {},
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
onExitOffline: () -> Unit = {},
|
onExitOffline: () -> Unit = {},
|
||||||
downloadProgress: DownloadProgress? = null,
|
|
||||||
onItemLongClick: (MenuTarget) -> Unit = {},
|
onItemLongClick: (MenuTarget) -> Unit = {},
|
||||||
syncing: Boolean = false,
|
syncing: Boolean = false,
|
||||||
refreshing: Boolean = false,
|
refreshing: Boolean = false,
|
||||||
@@ -602,8 +594,6 @@ fun LibraryScreen(
|
|||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
offline = offline,
|
offline = offline,
|
||||||
onExitOffline = onExitOffline,
|
onExitOffline = onExitOffline,
|
||||||
downloadProgress = downloadProgress,
|
|
||||||
onOpenDownloads = onOpenDownloads,
|
|
||||||
artwork = artwork,
|
artwork = artwork,
|
||||||
syncing = syncing,
|
syncing = syncing,
|
||||||
)
|
)
|
||||||
@@ -868,8 +858,6 @@ internal fun LibraryTabs(
|
|||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
onExitOffline: () -> Unit = {},
|
onExitOffline: () -> Unit = {},
|
||||||
downloadProgress: DownloadProgress? = null,
|
|
||||||
onOpenDownloads: () -> Unit = {},
|
|
||||||
artwork: ArtworkColors?,
|
artwork: ArtworkColors?,
|
||||||
syncing: Boolean = false,
|
syncing: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -953,14 +941,7 @@ internal fun LibraryTabs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
if (downloadProgress != null) {
|
if (offline) {
|
||||||
IconButton(onClick = onOpenDownloads) {
|
|
||||||
DownloadProgressIcon(
|
|
||||||
progress = downloadProgress,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (offline) {
|
|
||||||
IconButton(onClick = onExitOffline) {
|
IconButton(onClick = onExitOffline) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.CloudOff,
|
imageVector = Icons.Rounded.CloudOff,
|
||||||
@@ -991,41 +972,6 @@ internal fun LibraryTabs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DownloadProgressIcon(
|
|
||||||
progress: DownloadProgress,
|
|
||||||
color: Color,
|
|
||||||
) {
|
|
||||||
val description = stringResource(R.string.downloads_in_progress)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(32.dp).semantics { contentDescription = description },
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val fraction = progress.fraction
|
|
||||||
if (fraction != null) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
progress = { fraction },
|
|
||||||
color = color,
|
|
||||||
trackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = color,
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Download,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = color,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TabButton(
|
private fun TabButton(
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ import com.subtracks.data.model.Artist
|
|||||||
import com.subtracks.data.model.BulkDownloadAction
|
import com.subtracks.data.model.BulkDownloadAction
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.data.model.DownloadList
|
import com.subtracks.data.model.DownloadList
|
||||||
import com.subtracks.data.model.DownloadProgress
|
|
||||||
import com.subtracks.data.model.DownloadStatus
|
|
||||||
import com.subtracks.data.model.ListDownloadStatus
|
import com.subtracks.data.model.ListDownloadStatus
|
||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.model.SongDownload
|
|
||||||
import com.subtracks.data.prefs.AlbumSort
|
import com.subtracks.data.prefs.AlbumSort
|
||||||
import com.subtracks.data.prefs.ArtistSort
|
import com.subtracks.data.prefs.ArtistSort
|
||||||
import com.subtracks.data.prefs.LibraryListTab
|
import com.subtracks.data.prefs.LibraryListTab
|
||||||
@@ -118,13 +115,6 @@ class LibraryViewModel(
|
|||||||
|
|
||||||
val offline: StateFlow<Boolean> = sourceRepository.offline
|
val offline: StateFlow<Boolean> = sourceRepository.offline
|
||||||
|
|
||||||
val downloadProgress: StateFlow<DownloadProgress?> =
|
|
||||||
combine(
|
|
||||||
downloadRepository.activeDownloads(),
|
|
||||||
sourceRepository.activeSourceId(),
|
|
||||||
) { rows, sourceId -> downloadProgress(rows, sourceId) }
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
|
||||||
|
|
||||||
fun setOffline(enabled: Boolean) = sourceRepository.setOfflineMode(enabled)
|
fun setOffline(enabled: Boolean) = sourceRepository.setOfflineMode(enabled)
|
||||||
|
|
||||||
val syncing: StateFlow<Boolean> =
|
val syncing: StateFlow<Boolean> =
|
||||||
@@ -233,21 +223,6 @@ class LibraryViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun downloadProgress(
|
|
||||||
rows: List<SongDownload>,
|
|
||||||
sourceId: Long?,
|
|
||||||
): DownloadProgress? {
|
|
||||||
if (sourceId == null) return null
|
|
||||||
val active = rows.filter { it.sourceId == sourceId }
|
|
||||||
if (active.isEmpty()) return null
|
|
||||||
val running = active.filter { it.status == DownloadStatus.Running }
|
|
||||||
if (running.isEmpty() || running.any { it.total <= 0 }) return DownloadProgress(bytes = 0, total = 0)
|
|
||||||
return DownloadProgress(
|
|
||||||
bytes = running.sumOf { it.bytes },
|
|
||||||
total = running.sumOf { it.total },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun ListQuery.albumSort(): AlbumSort = AlbumSort.entries.firstOrNull { it.name == sort } ?: AlbumSort.Name
|
internal fun ListQuery.albumSort(): AlbumSort = AlbumSort.entries.firstOrNull { it.name == sort } ?: AlbumSort.Name
|
||||||
|
|
||||||
internal fun ListQuery.artistSort(): ArtistSort = ArtistSort.entries.firstOrNull { it.name == sort } ?: ArtistSort.Name
|
internal fun ListQuery.artistSort(): ArtistSort = ArtistSort.entries.firstOrNull { it.name == sort } ?: ArtistSort.Name
|
||||||
|
|||||||
@@ -176,7 +176,6 @@
|
|||||||
<string name="filter_downloaded">Downloaded</string>
|
<string name="filter_downloaded">Downloaded</string>
|
||||||
<string name="close_search">Close search</string>
|
<string name="close_search">Close search</string>
|
||||||
<string name="offline_mode_status">Offline mode; tap to go online</string>
|
<string name="offline_mode_status">Offline mode; tap to go online</string>
|
||||||
<string name="downloads_in_progress">Downloads in progress; tap to open the download manager</string>
|
|
||||||
<string name="shuffle_artist">Shuffle artist</string>
|
<string name="shuffle_artist">Shuffle artist</string>
|
||||||
|
|
||||||
<string name="status_playing">Playing</string>
|
<string name="status_playing">Playing</string>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.subtracks.ui.components
|
package com.subtracks.ui.components
|
||||||
|
|
||||||
import com.subtracks.data.model.DownloadProgress
|
|
||||||
import com.subtracks.data.model.ListDownloadStatus
|
import com.subtracks.data.model.ListDownloadStatus
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -38,14 +35,4 @@ class DownloadIndicatorTest {
|
|||||||
fun aFullyDownloadedListIsNotDownloading() {
|
fun aFullyDownloadedListIsNotDownloading() {
|
||||||
assertFalse(ListDownloadStatus(total = 4, downloaded = 4).isDownloading())
|
assertFalse(ListDownloadStatus(total = 4, downloaded = 4).isDownloading())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun totalDownloadProgressCombinesEveryInFlightTransfer() {
|
|
||||||
assertEquals(0.4f, DownloadProgress(bytes = 40, total = 100).fraction)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun queuedDownloadsWithoutASizeShowAnIndeterminateRing() {
|
|
||||||
assertNull(DownloadProgress(bytes = 0, total = 0).fraction)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package com.subtracks.ui.library
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.subtracks.data.model.DownloadProgress
|
|
||||||
import com.subtracks.data.model.DownloadStatus
|
|
||||||
import com.subtracks.data.model.SongDownload
|
|
||||||
import com.subtracks.data.prefs.AlbumSort
|
import com.subtracks.data.prefs.AlbumSort
|
||||||
import com.subtracks.data.prefs.ArtistSort
|
import com.subtracks.data.prefs.ArtistSort
|
||||||
import com.subtracks.data.prefs.ListQuery
|
import com.subtracks.data.prefs.ListQuery
|
||||||
import com.subtracks.data.prefs.PlaylistSort
|
import com.subtracks.data.prefs.PlaylistSort
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@@ -28,39 +24,4 @@ class LibraryViewModelTest {
|
|||||||
assertEquals(ArtistSort.Name, ListQuery("LegacyArtistOrder").artistSort())
|
assertEquals(ArtistSort.Name, ListQuery("LegacyArtistOrder").artistSort())
|
||||||
assertEquals(PlaylistSort.Name, ListQuery("LegacyPlaylistOrder").playlistSort())
|
assertEquals(PlaylistSort.Name, ListQuery("LegacyPlaylistOrder").playlistSort())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun downloadProgressOnlyCountsTheActiveSource() {
|
|
||||||
val rows = listOf(download(1, bytes = 40, total = 100), download(2, bytes = 10, total = 20))
|
|
||||||
|
|
||||||
assertEquals(DownloadProgress(bytes = 40, total = 100), downloadProgress(rows, sourceId = 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun anotherSourcesDownloadsDoNotShowAProgressRing() {
|
|
||||||
assertNull(downloadProgress(listOf(download(2, bytes = 10, total = 20)), sourceId = 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun aTransferWithoutAKnownSizeIsIndeterminate() {
|
|
||||||
val rows =
|
|
||||||
listOf(
|
|
||||||
download(1, bytes = 100, total = 100),
|
|
||||||
download(1, bytes = 20, total = 0).copy(songId = "s2"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(DownloadProgress(bytes = 0, total = 0), downloadProgress(rows, sourceId = 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun download(
|
|
||||||
sourceId: Long,
|
|
||||||
bytes: Long,
|
|
||||||
total: Long,
|
|
||||||
) = SongDownload(
|
|
||||||
sourceId = sourceId,
|
|
||||||
songId = "s1",
|
|
||||||
status = DownloadStatus.Running,
|
|
||||||
bytes = bytes,
|
|
||||||
total = total,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user