show the downloaded song count on a playlist offline

This commit is contained in:
austinried
2026-10-04 00:30:04 +00:00
parent aa956668b1
commit a36b310f95
4 changed files with 64 additions and 9 deletions
@@ -97,6 +97,17 @@ internal const val PLAYLISTS_FILTER =
"WHERE playlist_search MATCH '\"' || replace(:search, '\"', '\"\"') || '\"')) " +
"OR (length(:search) < 3 AND instr(lower(name), lower(:search)) > 0)) "
// With the Downloaded filter on, count the songs that actually have a download so the number under
// a playlist matches the songs its detail screen lists.
internal const val PLAYLISTS_SELECT =
"SELECT playlists.sourceId, playlists.id, playlists.name, playlists.comment, playlists.coverArt, " +
"CASE WHEN :downloadedFilter = 1 THEN (" +
"SELECT COUNT(DISTINCT ps.songId) FROM playlist_songs ps " +
"JOIN song_downloads sd ON sd.sourceId = ps.sourceId AND sd.songId = ps.songId AND sd.status = 'Completed' " +
"WHERE ps.sourceId = playlists.sourceId AND ps.playlistId = playlists.id " +
") ELSE playlists.songCount END AS songCount, " +
"playlists.created, playlists.changed, playlists.duration "
internal const val PLAYLIST_ORDER_BY_NAME = "name COLLATE NOCASE, id"
internal const val PLAYLIST_ORDER_BY_NAME_REVERSED = "name COLLATE NOCASE DESC, id DESC"
internal const val PLAYLIST_ORDER_BY_ADDED = "created DESC, name COLLATE NOCASE, id"
@@ -364,42 +375,42 @@ interface LibraryDao {
downloadedFilter: Int = 0,
): PagingSource<Int, Artist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME")
fun playlistsByName(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_NAME_REVERSED")
fun playlistsByNameReversed(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED")
fun playlistsByAdded(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_ADDED_REVERSED")
fun playlistsByAddedReversed(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED")
fun playlistsByUpdated(
sourceId: Long,
search: String,
downloadedFilter: Int = 0,
): PagingSource<Int, Playlist>
@Query("SELECT * $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED")
@Query("$PLAYLISTS_SELECT $PLAYLISTS_FILTER ORDER BY $PLAYLIST_ORDER_BY_UPDATED_REVERSED")
fun playlistsByUpdatedReversed(
sourceId: Long,
search: String,
@@ -51,6 +51,7 @@ fun PlaylistDetailRoute(
val playlist by viewModel.playlist.collectAsStateWithLifecycle(initialValue = null)
val downloads by viewModel.downloads.collectAsStateWithLifecycle()
val downloadStatus by viewModel.downloadStatus.collectAsStateWithLifecycle()
val offline by viewModel.offline.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<Long?>(null) }
val playback by playbackController.state.collectAsStateWithLifecycle()
@@ -81,6 +82,7 @@ fun PlaylistDetailRoute(
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
downloads = downloads,
downloadStatus = downloadStatus,
offline = offline,
onDownloadAction = requestDownloadAction,
onBack = onBack,
onSongClick = viewModel::play,
@@ -111,6 +113,7 @@ fun PlaylistDetailScreen(
artwork: ArtworkColors?,
downloads: Map<String, SongDownload> = emptyMap(),
downloadStatus: ListDownloadStatus = ListDownloadStatus(),
offline: Boolean = false,
onDownloadAction: (BulkDownloadAction) -> Unit = {},
onBack: () -> Unit,
onSongClick: (Long) -> Unit,
@@ -130,7 +133,7 @@ fun PlaylistDetailScreen(
art = coverArt(playlist?.coverArt, false),
thumbnailRef = coverArt(playlist?.coverArt, true),
name = playlist?.name.orEmpty(),
subtitle = playlistSummary(playlist),
subtitle = playlistSummary(playlist, offline, downloadStatus.downloaded),
comment = playlist?.comment,
hasSongs = songs.itemCount > 0,
onPlay = onPlay,
@@ -168,10 +171,15 @@ fun PlaylistDetailScreen(
)
}
private fun playlistSummary(playlist: Playlist?): String {
private fun playlistSummary(
playlist: Playlist?,
offline: Boolean,
downloadedSongs: Long,
): String {
playlist ?: return ""
val songs = if (offline) downloadedSongs else playlist.songCount
return listOfNotNull(
"${playlist.songCount} ${if (playlist.songCount == 1L) "song" else "songs"}",
"$songs ${if (songs == 1L) "song" else "songs"}",
formatDuration(playlist.duration),
).joinToString(" $DOT ")
}
@@ -47,6 +47,8 @@ class PlaylistDetailViewModel(
val downloads: StateFlow<Map<String, SongDownload>> =
downloadRepository.states()
val offline: StateFlow<Boolean> = sourceRepository.offline
val downloadStatus: StateFlow<ListDownloadStatus> =
sourceId
.flatMapLatest { downloadRepository.status(it, DownloadList.Playlist, playlistId) }
@@ -419,6 +419,40 @@ class LibraryDaoTest {
)
}
@Test
fun downloadedFilterReportsTheDownloadedSongCountForPlaylists() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertPlaylists(listOf(playlist(sourceId, "pl-1", "List", created = 0, changed = 0).copy(songCount = 5)))
dao.upsertSongs(
listOf(
song(sourceId, "s1", "One", starred = null),
song(sourceId, "s2", "Two", starred = null),
),
)
dao.upsertPlaylistSongs(
listOf(
PlaylistSong(sourceId, "pl-1", "s1", 0),
PlaylistSong(sourceId, "pl-1", "s2", 1),
),
)
db.downloadDao().upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
val filtered = dao.playlistsByName(sourceId, "", downloadedFilter = 1).page()
assertEquals(1, filtered.size)
assertEquals(1L, filtered.first().songCount)
assertEquals(
5L,
dao
.playlistsByName(sourceId, "")
.page()
.first { it.id == "pl-1" }
.songCount,
)
}
@Test
fun searchMatchesInfixCaseInsensitivelyAndFallsBackToAScanBelowThreeCharacters() =
runTest {