drop the songs tab and the whole-library song queue

This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent 9cf2c99fb5
commit e3af5ed0ac
25 changed files with 36 additions and 927 deletions
@@ -23,27 +23,6 @@ internal const val PLAYLIST_SONGS_SQL =
"WHERE playlist_songs.sourceId = :sourceId AND playlist_songs.playlistId = :playlistId " +
"ORDER BY playlist_songs.position"
internal const val SONGS_SQL =
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
"songs.title COLLATE NOCASE, songs.id"
internal const val SONGS_IDS_SELECT =
"SELECT songs.id FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) "
internal const val SONGS_LIST_SELECT =
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) "
@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface LibraryDao {
@@ -431,168 +410,6 @@ interface LibraryDao {
search: String,
): PagingSource<Int, Playlist>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
"songs.title COLLATE NOCASE, songs.id",
)
fun songs(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, songs.disc DESC, " +
"songs.track DESC, songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id DESC",
)
fun songsByAlbumReversed(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByTitle(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByTitleReversed(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByArtist(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByArtistReversed(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.starred IS NULL, songs.starred DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByStarred(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.starred IS NULL, songs.starred ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByStarredReversed(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.created DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByAdded(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.created ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
fun songsByAddedReversed(
sourceId: Long,
starredFilter: Int,
search: String,
): PagingSource<Int, SongListItem>
@Query(PLAYLIST_SONGS_SQL)
fun playlistSongs(
sourceId: Long,
@@ -640,181 +457,4 @@ interface LibraryDao {
sourceId: Long,
albumId: String,
): Flow<List<Song>>
@Query(
"SELECT COUNT(*) FROM songs WHERE songs.sourceId = :sourceId AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR (:starredFilter = 2 AND songs.starred IS NULL))",
)
suspend fun songCountForFilter(
sourceId: Long,
starredFilter: Int,
): Long
@Query(
"${SONGS_IDS_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.title COLLATE NOCASE, songs.id",
)
suspend fun songIdsByAlbum(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query(
"${SONGS_IDS_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, " +
"songs.disc DESC, songs.track DESC, songs.title COLLATE NOCASE DESC, songs.id DESC",
)
suspend fun songIdsByAlbumReversed(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByTitle(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByTitleReversed(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByArtist(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByArtistReversed(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query(
"${SONGS_IDS_SELECT}ORDER BY songs.starred IS NULL, songs.starred DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
suspend fun songIdsByStarred(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query(
"${SONGS_IDS_SELECT}ORDER BY songs.starred IS NULL, songs.starred ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
)
suspend fun songIdsByStarredReversed(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.created DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByAdded(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query("${SONGS_IDS_SELECT}ORDER BY songs.created ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id")
suspend fun songIdsByAddedReversed(
sourceId: Long,
starredFilter: Int,
): List<String>
@Query(
"${SONGS_LIST_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByAlbum(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, songs.disc DESC, songs.track DESC, songs.title COLLATE NOCASE DESC, songs.id DESC LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByAlbumReversed(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByTitle(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByTitleReversed(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByArtist(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByArtistReversed(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.starred IS NULL, songs.starred DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByStarred(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.starred IS NULL, songs.starred ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByStarredReversed(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.created DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByAdded(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
@Query(
"${SONGS_LIST_SELECT}ORDER BY songs.created ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id LIMIT :limit OFFSET :offset",
)
suspend fun songsPageByAddedReversed(
sourceId: Long,
starredFilter: Int,
limit: Int,
offset: Long,
): List<SongListItem>
}
@@ -62,9 +62,6 @@ interface QueueDao {
albumId: String,
): Long
@Query("SELECT COUNT(*) FROM songs WHERE sourceId = :sourceId")
suspend fun songsLength(sourceId: Long): Long
@Query("SELECT COUNT(*) FROM songs WHERE sourceId = :sourceId AND id = :songId")
suspend fun songLength(
sourceId: Long,
@@ -87,13 +84,6 @@ interface QueueDao {
limit: Int,
): List<SongListItem>
@Query("$SONGS_SQL LIMIT :limit OFFSET :offset")
suspend fun songs(
sourceId: Long,
offset: Long,
limit: Int,
): List<SongListItem>
@Query("$SONG_SQL LIMIT :limit OFFSET :offset")
suspend fun song(
sourceId: Long,
@@ -111,15 +101,6 @@ interface QueueDao {
albumId: String,
): List<String>
@Query(
"SELECT songs.id FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " +
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
"songs.title COLLATE NOCASE, songs.id",
)
suspend fun songIds(sourceId: Long): List<String>
@Query(
"SELECT songs.id FROM playlist_songs " +
"JOIN songs ON songs.sourceId = playlist_songs.sourceId AND songs.id = playlist_songs.songId " +
@@ -191,7 +191,7 @@ data class DiscKey(
val disc: Long,
)
enum class QueueKind { Playlist, Album, Song, Songs }
enum class QueueKind { Playlist, Album, Song }
@Entity(
tableName = "queue_entries",
@@ -261,5 +261,5 @@ class QueueKindConverter {
fun fromQueueKind(kind: QueueKind): String = kind.name
@ColumnTypeConverter
fun toQueueKind(value: String): QueueKind = QueueKind.valueOf(value)
fun toQueueKind(value: String): QueueKind = QueueKind.entries.firstOrNull { it.name == value } ?: QueueKind.Song
}
@@ -16,8 +16,6 @@ enum class ArtistSort { Name, AlbumCount, Starred }
enum class PlaylistSort { Name, Added, Updated }
enum class SongSort { Album, Title, Artist, Starred, Added }
enum class LibraryListTab(
val key: String,
val defaultSort: String,
@@ -26,7 +24,6 @@ enum class LibraryListTab(
Albums("albums", AlbumSort.Name.name, true),
Artists("artists", ArtistSort.Name.name, true),
Playlists("playlists", PlaylistSort.Name.name, false),
Songs("songs", SongSort.Album.name, true),
}
enum class StarredFilter { Any, Starred, NotStarred }
@@ -14,7 +14,6 @@ import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.StarredFilter
import com.subtracks.data.source.StarType
import kotlinx.coroutines.CancellationException
@@ -155,58 +154,6 @@ class LibraryRepository(
}
}
fun songs(
sourceId: Long,
sort: SongSort,
descending: Boolean,
starred: StarredFilter,
search: String,
): Flow<PagingData<SongListItem>> =
pager(60) {
val dao = db.libraryDao()
when (sort) {
SongSort.Album -> {
if (descending) {
dao.songsByAlbumReversed(sourceId, starred.ordinal, search)
} else {
dao.songs(sourceId, starred.ordinal, search)
}
}
SongSort.Title -> {
if (descending) {
dao.songsByTitleReversed(sourceId, starred.ordinal, search)
} else {
dao.songsByTitle(sourceId, starred.ordinal, search)
}
}
SongSort.Artist -> {
if (descending) {
dao.songsByArtistReversed(sourceId, starred.ordinal, search)
} else {
dao.songsByArtist(sourceId, starred.ordinal, search)
}
}
SongSort.Starred -> {
if (descending) {
dao.songsByStarredReversed(sourceId, starred.ordinal, search)
} else {
dao.songsByStarred(sourceId, starred.ordinal, search)
}
}
SongSort.Added -> {
if (descending) {
dao.songsByAddedReversed(sourceId, starred.ordinal, search)
} else {
dao.songsByAdded(sourceId, starred.ordinal, search)
}
}
}
}
fun album(
sourceId: Long,
albumId: String,
@@ -4,15 +4,12 @@ import androidx.room3.deferredTransaction
import androidx.room3.immediateTransaction
import androidx.room3.useReaderConnection
import androidx.room3.useWriterConnection
import com.subtracks.data.db.LibraryDao
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.model.PlaybackCursor
import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.ShuffleOrder
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.StarredFilter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -20,12 +17,6 @@ import java.util.concurrent.atomic.AtomicLong
const val QUEUE_CHUNK = 60
data class QueueSongQuery(
val sort: SongSort,
val descending: Boolean,
val starred: StarredFilter,
)
data class ResolvedQueueEntry(
val entry: QueueEntry,
val length: Long,
@@ -100,26 +91,6 @@ class QueueRepository(
songId: String,
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = songId)
fun songsEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Songs, refId = "")
fun songsEntry(
sourceId: Long,
query: QueueSongQuery,
) = QueueEntry(
position = 0,
sourceId = sourceId,
kind = QueueKind.Songs,
refId = "${query.sort.name}|${if (query.descending) 1 else 0}|${query.starred.ordinal}",
)
suspend fun songsIndexOf(
entry: QueueEntry,
songId: String,
): Long? {
val index = songIdsFor(entry).indexOf(songId)
return if (index >= 0) index.toLong() else null
}
suspend fun sourceName(
kind: QueueKind,
sourceId: Long,
@@ -143,117 +114,12 @@ class QueueRepository(
?.takeIf { it.isNotBlank() }
}
QueueKind.Songs, QueueKind.Song -> {
QueueKind.Song -> {
null
}
}
}
private fun songsQuery(refId: String): QueueSongQuery? {
val parts = refId.split("|")
if (parts.size != 3) return null
val sort = SongSort.entries.firstOrNull { it.name == parts[0] } ?: return null
val starred = StarredFilter.entries.getOrElse(parts[2].toIntOrNull() ?: 0) { StarredFilter.Any }
return QueueSongQuery(sort, parts[1] == "1", starred)
}
private suspend fun songIdsFor(entry: QueueEntry): List<String> {
val query = songsQuery(entry.refId) ?: return dao.songIds(entry.sourceId)
val library = db.libraryDao()
val starred = query.starred.ordinal
return when (query.sort) {
SongSort.Album -> {
if (query.descending) {
library.songIdsByAlbumReversed(entry.sourceId, starred)
} else {
library.songIdsByAlbum(entry.sourceId, starred)
}
}
SongSort.Title -> {
if (query.descending) {
library.songIdsByTitleReversed(entry.sourceId, starred)
} else {
library.songIdsByTitle(entry.sourceId, starred)
}
}
SongSort.Artist -> {
if (query.descending) {
library.songIdsByArtistReversed(entry.sourceId, starred)
} else {
library.songIdsByArtist(entry.sourceId, starred)
}
}
SongSort.Starred -> {
if (query.descending) {
library.songIdsByStarredReversed(entry.sourceId, starred)
} else {
library.songIdsByStarred(entry.sourceId, starred)
}
}
SongSort.Added -> {
if (query.descending) {
library.songIdsByAddedReversed(entry.sourceId, starred)
} else {
library.songIdsByAdded(entry.sourceId, starred)
}
}
}
}
private suspend fun LibraryDao.songsPage(
sourceId: Long,
query: QueueSongQuery,
limit: Int,
offset: Long,
): List<SongListItem> {
val starred = query.starred.ordinal
return when (query.sort) {
SongSort.Album -> {
if (query.descending) {
songsPageByAlbumReversed(sourceId, starred, limit, offset)
} else {
songsPageByAlbum(sourceId, starred, limit, offset)
}
}
SongSort.Title -> {
if (query.descending) {
songsPageByTitleReversed(sourceId, starred, limit, offset)
} else {
songsPageByTitle(sourceId, starred, limit, offset)
}
}
SongSort.Artist -> {
if (query.descending) {
songsPageByArtistReversed(sourceId, starred, limit, offset)
} else {
songsPageByArtist(sourceId, starred, limit, offset)
}
}
SongSort.Starred -> {
if (query.descending) {
songsPageByStarredReversed(sourceId, starred, limit, offset)
} else {
songsPageByStarred(sourceId, starred, limit, offset)
}
}
SongSort.Added -> {
if (query.descending) {
songsPageByAddedReversed(sourceId, starred, limit, offset)
} else {
songsPageByAdded(sourceId, starred, limit, offset)
}
}
}
}
suspend fun replace(
entries: List<QueueEntry>,
shuffleOrder: LongArray? = null,
@@ -476,7 +342,6 @@ class QueueRepository(
when (entry.kind) {
QueueKind.Song -> listOf(entry.refId)
QueueKind.Album -> dao.albumSongIds(entry.sourceId, entry.refId)
QueueKind.Songs -> songIdsFor(entry)
QueueKind.Playlist -> dao.playlistSongIds(entry.sourceId, entry.refId)
}
val from = entry.offset.toInt().coerceIn(0, entryIds.size)
@@ -502,12 +367,6 @@ class QueueRepository(
dao.albumSongs(entry.sourceId, entry.refId, offset, limit)
}
QueueKind.Songs -> {
songsQuery(entry.refId)?.let { query ->
db.libraryDao().songsPage(entry.sourceId, query, limit, offset)
} ?: dao.songs(entry.sourceId, offset, limit)
}
QueueKind.Song -> {
dao.song(entry.sourceId, entry.refId, offset, limit)
}
@@ -527,11 +386,6 @@ class QueueRepository(
QueueKind.Album -> {
dao.albumLength(sourceId, refId)
}
QueueKind.Songs -> {
songsQuery(refId)?.let { db.libraryDao().songCountForFilter(sourceId, it.starred.ordinal) }
?: dao.songsLength(sourceId)
}
}
val available = (total - offset).coerceAtLeast(0)
return count?.coerceAtMost(available) ?: available
@@ -46,7 +46,7 @@ fun appModule(
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
single { PlaybackController(get(), get(), get()) }
viewModel { RootViewModel(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
viewModel { LibraryViewModel(get(), get(), get(), get()) }
viewModel { SettingsViewModel(get(), get()) }
viewModel { AddSourceViewModel(get(), get()) }
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
@@ -5,7 +5,6 @@ import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind
import com.subtracks.data.repo.QueueRepository
import com.subtracks.data.repo.QueueSnapshot
import com.subtracks.data.repo.QueueSongQuery
import com.subtracks.data.repo.QueueWindowItem
import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.CoroutineDispatcher
@@ -190,23 +189,6 @@ class PlaybackController(
playlistId: String,
) = play(listOf(queueRepository.playlistEntry(sourceId, playlistId)), 0, disableShuffle = true)
fun playSongs(
sourceId: Long,
startPosition: Long,
) = play(listOf(queueRepository.songsEntry(sourceId)), startPosition)
fun playSong(
sourceId: Long,
songId: String,
query: QueueSongQuery,
) {
scope.launch {
val entry = queueRepository.songsEntry(sourceId, query)
val index = queueRepository.songsIndexOf(entry, songId) ?: return@launch
play(listOf(entry), index)
}
}
fun shuffleAlbum(
sourceId: Long,
albumId: String,
@@ -33,7 +33,6 @@ import androidx.compose.material.icons.rounded.ArrowDownward
import androidx.compose.material.icons.rounded.ArrowUpward
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.FilterAltOff
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Remove
import androidx.compose.material.icons.rounded.Search
@@ -104,13 +103,11 @@ import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.LibraryListTab
import com.subtracks.data.prefs.ListQuery
import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.StarredFilter
import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController
@@ -135,7 +132,6 @@ enum class LibraryTab(
Albums("Albums", Icons.Rounded.Album),
Artists("Artists", Icons.Rounded.Person),
Playlists("Playlists", Icons.AutoMirrored.Rounded.PlaylistPlay),
Songs("Songs", Icons.Rounded.MusicNote),
}
data class SortOption(
@@ -173,16 +169,6 @@ fun sortOptionsFor(tab: LibraryTab): List<SortOption> =
SortOption(PlaylistSort.Updated.name, "Updated", descendingByDefault = true),
)
}
LibraryTab.Songs -> {
listOf(
SortOption(SongSort.Album.name, "Album"),
SortOption(SongSort.Title.name, "Title"),
SortOption(SongSort.Artist.name, "Artist"),
SortOption(SongSort.Starred.name, "Starred", descendingByDefault = true),
SortOption(SongSort.Added.name, "Added", descendingByDefault = true),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -203,7 +189,6 @@ fun LibraryRoute(
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
var previousTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
val listTab = selectedTab.listTab()
LaunchedEffect(selectedTab) {
if (previousTab != selectedTab) {
@@ -240,17 +225,14 @@ fun LibraryRoute(
artwork = artwork,
albums = viewModel.albums,
artists = viewModel.artists,
songs = viewModel.songs,
playlists = viewModel.playlists,
coverArt = viewModel::coverArt,
onAlbumClick = onAlbumClick,
onArtistClick = onArtistClick,
onPlaylistClick = onPlaylistClick,
onSongClick = viewModel::playSong,
onSync = viewModel::sync,
onOpenSettings = onOpenSettings,
onItemLongClick = { contextMenuHost?.show(it, itemActions) },
playingSongId = playingSongId,
listQuery = listQuery,
resetKeys = resetKeys,
sortOptions = sortOptionsFor(selectedTab),
@@ -282,18 +264,15 @@ fun LibraryScreen(
onTabSelected: (LibraryTab) -> Unit,
albums: Flow<PagingData<Album>>,
artists: Flow<PagingData<Artist>>,
songs: Flow<PagingData<SongListItem>>,
playlists: Flow<PagingData<Playlist>>,
coverArt: (String?, Boolean) -> CoverArtRef?,
onAlbumClick: (Album) -> Unit,
onArtistClick: (Artist) -> Unit,
onPlaylistClick: (Playlist) -> Unit,
onSongClick: (String) -> Unit,
onSync: () -> Unit,
onOpenSettings: () -> Unit,
onItemLongClick: (MenuTarget) -> Unit = {},
syncing: Boolean = false,
playingSongId: String? = null,
bottomInset: Dp = 0.dp,
listQuery: ListQuery = ListQuery(""),
resetKeys: Map<LibraryTab, Any?> = emptyMap(),
@@ -411,25 +390,6 @@ fun LibraryScreen(
)
}
LibraryTab.Songs -> {
SongsContent(
songs.collectAsLazyPagingItems(),
coverArt,
listBottomInset,
{
dismissSearch()
onSongClick(it)
},
onLongClick = onItemLongClick,
playingSongId,
filtered = filtersActive,
onClearFilters = onClearFilters,
resetKey = resetKey,
topInset = listTopInset,
onSync = onSync,
)
}
LibraryTab.Playlists -> {
PlaylistsContent(
playlists.collectAsLazyPagingItems(),
@@ -8,21 +8,16 @@ import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.AlbumSort
import com.subtracks.data.prefs.ArtistSort
import com.subtracks.data.prefs.LibraryListTab
import com.subtracks.data.prefs.ListQuery
import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.UserPreferences
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.QueueSongQuery
import com.subtracks.data.repo.SourceRepository
import com.subtracks.data.sync.SyncManager
import com.subtracks.data.sync.SyncStatus
import com.subtracks.playback.PlaybackController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,7 +35,6 @@ class LibraryViewModel(
private val libraryRepository: LibraryRepository,
private val sourceRepository: SourceRepository,
private val syncManager: SyncManager,
private val playbackController: PlaybackController,
private val userPreferences: UserPreferences,
) : ViewModel() {
private val listQueries: Map<LibraryListTab, StateFlow<ListQuery>> =
@@ -86,27 +80,11 @@ class LibraryViewModel(
}
}.cachedIn(viewModelScope)
val songs: Flow<PagingData<SongListItem>> =
libraryRepository.activeSourceId
.filterNotNull()
.flatMapLatest { sourceId ->
combine(listQueries.getValue(LibraryListTab.Songs), searches.getValue(LibraryListTab.Songs)) { query, search ->
query to search
}.flatMapLatest { (query, search) ->
libraryRepository.songs(sourceId, query.songSort(), query.descending, query.starred, search)
}
}.cachedIn(viewModelScope)
val syncing: StateFlow<Boolean> =
syncManager.status
.map { it == SyncStatus.Running }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
val playingSongId: StateFlow<String?> =
playbackController.state
.map { state -> state.item?.id?.takeIf { state.context?.kind == QueueKind.Songs } }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
fun listQuery(tab: LibraryListTab): StateFlow<ListQuery> = listQueries.getValue(tab)
fun setListQuery(
@@ -130,14 +108,6 @@ class LibraryViewModel(
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
fun playSong(songId: String) {
viewModelScope.launch {
val sourceId = sourceRepository.activeSourceIdOnce() ?: return@launch
val query = listQueries.getValue(LibraryListTab.Songs).value
playbackController.playSong(sourceId, songId, QueueSongQuery(query.songSort(), query.descending, query.starred))
}
}
fun playAlbum(albumId: String) {
viewModelScope.launch {
sourceRepository.activeSourceIdOnce()?.let { playbackController.playAlbum(it, albumId, 0) }
@@ -170,5 +140,3 @@ private fun ListQuery.albumSort(): AlbumSort = AlbumSort.entries.firstOrNull { i
private fun ListQuery.artistSort(): ArtistSort = ArtistSort.entries.firstOrNull { it.name == sort } ?: ArtistSort.Name
private fun ListQuery.playlistSort(): PlaylistSort = PlaylistSort.entries.firstOrNull { it.name == sort } ?: PlaylistSort.Name
private fun ListQuery.songSort(): SongSort = SongSort.entries.firstOrNull { it.name == sort } ?: SongSort.Album
@@ -1,17 +1,10 @@
package com.subtracks.ui.library
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PlayArrow
@@ -27,80 +20,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.Song
import com.subtracks.data.model.SongListItem
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.FilteredEmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.ResetScrollOnChange
import com.subtracks.ui.components.rememberViewportFill
@Composable
fun SongsContent(
items: LazyPagingItems<SongListItem>,
coverArt: (String?, Boolean) -> CoverArtRef?,
bottomInset: Dp,
onSongClick: (String) -> Unit,
onLongClick: (MenuTarget) -> Unit = {},
playingSongId: String? = null,
filtered: Boolean = false,
onClearFilters: () -> Unit = {},
resetKey: Any? = null,
topInset: Dp = 0.dp,
onSync: () -> Unit = {},
modifier: Modifier = Modifier,
) {
when {
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
LoadingState(modifier)
}
items.itemCount == 0 -> {
if (filtered) {
FilteredEmptyState(onClearFilters, modifier)
} else {
EmptyState("No songs yet.", modifier, actionLabel = "Sync", onAction = onSync)
}
}
else -> {
val listState = rememberLazyListState()
ResetScrollOnChange(resetKey, { items.loadState.refresh }) { listState.scrollToItem(0) }
val fill = rememberViewportFill(listState)
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(top = topInset, bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
val item = items[index]
if (item != null) {
SongRow(
song = item.song,
coverArtId = item.coverArt,
coverArt = coverArt,
isPlaying = item.song.id == playingSongId,
modifier =
Modifier.combinedClickable(
onClick = { onSongClick(item.song.id) },
onLongClick = { onLongClick(MenuTarget.Song(item.song, coverArt(item.coverArt, true))) },
),
)
}
}
item { Spacer(Modifier.height(fill)) }
}
}
}
}
@Composable
fun SongRow(
@@ -112,12 +112,7 @@ fun NowPlayingRoute(
val art = controller.coverArt(state.item)
val thumbnail = controller.coverArt(state.item, thumbnail = true)
val queueContext = state.context
val fallbackTitle =
if (queueContext?.kind == QueueKind.Songs) {
"Library"
} else {
state.item?.album?.takeIf { it.isNotBlank() } ?: "Library"
}
val fallbackTitle = state.item?.album?.takeIf { it.isNotBlank() } ?: "Library"
val sourceTitle = remember(queueContext) { mutableStateOf(fallbackTitle) }
LaunchedEffect(queueContext) {
sourceTitle.value = controller.sourceTitle(queueContext)?.takeIf { it.isNotBlank() } ?: fallbackTitle
@@ -443,7 +438,6 @@ private fun QueueKind.label(): String =
QueueKind.Album -> "album"
QueueKind.Playlist -> "playlist"
QueueKind.Song -> "song"
QueueKind.Songs -> "songs"
}
private fun formatTime(milliseconds: Long): String {
@@ -9,6 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.Album
import com.subtracks.data.model.Song
import com.subtracks.data.model.Source
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
@@ -63,29 +64,6 @@ class LibraryDaoTest {
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredFilter = 1, search = "").page().map { it.name })
}
@Test
fun songSortsByTitleAndStarredFilter() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertSongs(
listOf(
song(sourceId, "s1", "Zebra", starred = null),
song(sourceId, "s2", "Apple", starred = 7),
),
)
assertEquals(
listOf("Apple", "Zebra"),
dao.songsByTitle(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
)
assertEquals(
listOf("Zebra", "Apple"),
dao.songsByTitleReversed(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
)
assertEquals(listOf("Apple"), dao.songsByTitle(sourceId, starredFilter = 1, search = "").page().map { it.song.title })
}
@Test
fun albumAddedAndStarredSortsAndSearch() =
runTest {
@@ -112,24 +90,21 @@ class LibraryDaoTest {
}
@Test
fun songAddedSortOrdersByCreated() =
fun albumSongsAreOrderedByDiscAndTrack() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertSongs(
listOf(
song(sourceId, "s1", "Zebra", starred = null, created = 100),
song(sourceId, "s2", "Apple", starred = null, created = 200),
song(sourceId, "s2", "Second", starred = null, track = 2),
song(sourceId, "s3", "Third", starred = null, track = 1, disc = 2),
song(sourceId, "s1", "First", starred = null, track = 1),
),
)
assertEquals(
listOf("Apple", "Zebra"),
dao.songsByAdded(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
)
assertEquals(
listOf("Zebra", "Apple"),
dao.songsByAddedReversed(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
listOf("s1", "s2", "s3"),
dao.songsByAlbum(sourceId, "al-1").first().map { it.id },
)
}
@@ -170,6 +145,8 @@ class LibraryDaoTest {
title: String,
starred: Long?,
created: Long = 0,
track: Long = 1,
disc: Long = 1,
) = Song(
sourceId = sourceId,
id = id,
@@ -179,8 +156,8 @@ class LibraryDaoTest {
album = "Album",
artist = "Artist",
duration = 200,
track = 1,
disc = 1,
track = track,
disc = disc,
starred = starred,
genre = null,
created = created,
@@ -21,15 +21,19 @@ class UserPreferencesTest {
val prefs = UserPreferences(store)
assertEquals(ListQuery("Name"), prefs.listQuery(LibraryListTab.Albums).first())
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
assertEquals(ListQuery("Name"), prefs.listQuery(LibraryListTab.Artists).first())
prefs.setListQuery(LibraryListTab.Albums, ListQuery("Added", descending = true, starred = StarredFilter.Starred))
prefs.setListQuery(LibraryListTab.Artists, ListQuery("AlbumCount", descending = true))
assertEquals(
ListQuery("Added", descending = true, starred = StarredFilter.Starred),
prefs.listQuery(LibraryListTab.Albums).first(),
)
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
assertEquals(
ListQuery("AlbumCount", descending = true),
prefs.listQuery(LibraryListTab.Artists).first(),
)
file.delete()
}
@@ -12,8 +12,6 @@ import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song
import com.subtracks.data.model.Source
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.StarredFilter
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
@@ -75,19 +73,6 @@ class QueueRepositoryTest {
assertEquals(listOf("s2", "s3"), resolveAll(snapshot))
}
@Test
fun theSongsEntryResolvesTheWholeLibraryInListOrder() =
runTest {
seedLibrary()
db.libraryDao().upsertAlbums(listOf(album("al0", "Zeroth Album", "Aardvark")))
db.libraryDao().upsertSongs(listOf(song("s0", "al0", track = 1, album = "Zeroth Album")))
val snapshot = repository.snapshotAfter(listOf(repository.songsEntry(1)))
assertEquals(6, snapshot.size)
assertEquals(listOf("s0", "s1", "s2", "s3", "s4", "s5"), resolveAll(snapshot))
}
@Test
fun windowSpansEntryBoundaries() =
runTest {
@@ -314,39 +299,6 @@ class QueueRepositoryTest {
assertEquals(listOf("s2", "s3", "s1"), resolveAll(repository.snapshot()))
}
@Test
fun theSongsEntryFollowsItsSortAndStarredFilter() =
runTest {
seedLibrary()
db.libraryDao().upsertSongs(
listOf(
song("s1", "al1", track = 1, album = "First Album", title = "Charlie", starred = 5),
song("s2", "al1", track = 2, album = "First Album", title = "Alpha"),
song("s3", "al1", track = 3, album = "First Album", title = "Bravo", starred = 7),
song("s4", "al2", track = 1, album = "Second Album", title = "Echo"),
song("s5", "al2", track = 2, album = "Second Album", title = "Delta", starred = 3),
),
)
val byTitle =
repository.snapshotAfter(
listOf(repository.songsEntry(1, QueueSongQuery(SongSort.Title, descending = false, StarredFilter.Any))),
)
assertEquals(listOf("s2", "s3", "s1", "s5", "s4"), resolveAll(byTitle))
val starredOnly =
repository.snapshotAfter(
listOf(repository.songsEntry(1, QueueSongQuery(SongSort.Starred, descending = false, StarredFilter.Starred))),
)
assertEquals(listOf("s3", "s1", "s5"), resolveAll(starredOnly))
val unstarred =
repository.snapshotAfter(
listOf(repository.songsEntry(1, QueueSongQuery(SongSort.Album, descending = false, StarredFilter.NotStarred))),
)
assertEquals(listOf("s2", "s4"), resolveAll(unstarred))
}
private suspend fun QueueRepository.snapshotAfter(entries: List<QueueEntry>): QueueSnapshot {
replace(entries)
return snapshot()
@@ -69,14 +69,7 @@ class SyncServiceTest {
.allRows()
.size,
)
assertEquals(
2,
db
.libraryDao()
.songs(1, 0, "")
.allRows()
.size,
)
assertEquals(2, db.libraryDao().songIds(1).size)
assertEquals(
1,
db
@@ -99,14 +92,7 @@ class SyncServiceTest {
.allRows()
.map { it.id },
)
assertEquals(
listOf("s1"),
db
.libraryDao()
.songs(1, 0, "")
.allRows()
.map { it.song.id },
)
assertEquals(listOf("s1"), db.libraryDao().songIds(1))
}
@Test
@@ -106,19 +106,6 @@ class PlaybackControllerTest {
assertEquals("cover-al1", runBlocking { controller.upcomingItem()?.coverArtId })
}
@Test
fun playingTheSongsListQueuesTheWholeLibrary() {
seedAlbum(60, sourceId = 1)
controller.playSongs(1, 30)
await { handle.operations.contains("play") }
assertEquals(51, handle.itemCount)
assertEquals(QueueContext(QueueKind.Songs, 1, ""), controller.state.value.context)
assertTrue(controller.state.value.hasNext)
assertTrue(controller.state.value.hasPrevious)
}
@Test
fun advancingShiftsTheWindowAndKeepsItBounded() {
seedAlbum(100, sourceId = 1)
@@ -107,13 +107,11 @@ class AppScreenshotTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums)) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
@@ -133,13 +131,6 @@ class AppScreenshotTest {
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_artists.png")
}
@Test
fun librarySongs() {
setLibraryContent(LibraryTab.Songs)
awaitText("Everything In Its Right Place")
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_songs.png")
}
@Test
fun libraryPlaylists() {
setLibraryContent(LibraryTab.Playlists)
@@ -166,13 +157,11 @@ class AppScreenshotTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(Fixtures.albums)) },
artists = remember { flowOf(PagingData.from(Fixtures.artists)) },
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) },
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
artwork = artwork,
@@ -473,13 +462,11 @@ class AppScreenshotTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(Fixtures.albums)) },
artists = remember { flowOf(PagingData.from(Fixtures.artists)) },
songs = remember { flowOf(PagingData.from(Fixtures.songItems)) },
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
@@ -14,7 +14,6 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.ListQuery
import com.subtracks.ui.library.ALBUM_COVER_TAG
import com.subtracks.ui.library.LibraryScreen
@@ -44,13 +43,11 @@ class LibraryOptionsScreenshotTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
listQuery = ListQuery("Name"),
@@ -17,7 +17,6 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.ListQuery
import com.subtracks.data.prefs.StarredFilter
import com.subtracks.ui.library.ALBUM_COVER_TAG
@@ -50,13 +49,11 @@ class LibraryOptionsTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
listQuery = query,
@@ -23,7 +23,6 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.ui.library.ALBUM_COVER_TAG
import com.subtracks.ui.library.LibraryScreen
import com.subtracks.ui.library.LibraryTab
@@ -60,13 +59,11 @@ class LibraryScreenTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = { synced = true },
onOpenSettings = { settingsOpened = true },
)
@@ -95,13 +92,11 @@ class LibraryScreenTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.empty<Album>(settledLoadStates)) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = { synced = true },
onOpenSettings = {},
)
@@ -129,11 +124,6 @@ class LibraryScreenTest {
collected += "artists"
emit(PagingData.empty())
}
val songs =
flow<PagingData<SongListItem>> {
collected += "songs"
emit(PagingData.empty())
}
val playlists =
flow<PagingData<Playlist>> {
collected += "playlists"
@@ -146,13 +136,11 @@ class LibraryScreenTest {
onTabSelected = {},
albums = albums,
artists = artists,
songs = songs,
playlists = playlists,
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
)
@@ -178,11 +166,6 @@ class LibraryScreenTest {
collected += "artists"
emit(PagingData.from(artists()))
}
val songs =
flow<PagingData<SongListItem>> {
collected += "songs"
emit(PagingData.empty())
}
val playlists =
flow<PagingData<Playlist>> {
collected += "playlists"
@@ -196,13 +179,11 @@ class LibraryScreenTest {
onTabSelected = { selectedTab.value = it },
albums = albums,
artists = artists,
songs = songs,
playlists = playlists,
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
)
@@ -223,8 +204,7 @@ class LibraryScreenTest {
assertTrue("returning to a tab should render its content", collected.contains("albums"))
assertTrue("the tab that was shown should collect its flow", collected.contains("artists"))
assertFalse("inactive tabs should not collect their flows", collected.contains("songs"))
assertFalse(collected.contains("playlists"))
assertFalse("inactive tabs should not collect their flows", collected.contains("playlists"))
}
@Test
@@ -247,7 +227,7 @@ class LibraryScreenTest {
composeRule.runOnIdle { pagerState.requestScrollToPage(1, step * 0.05f) }
composeRule.waitForIdle()
composeRule
.onNodeWithContentDescription(LibraryTab.Songs.label)
.onNodeWithContentDescription(LibraryTab.Playlists.label)
.fetchSemanticsNode()
.boundsInRoot.left
}
@@ -15,7 +15,6 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.ListQuery
import com.subtracks.ui.library.ALBUM_COVER_TAG
import com.subtracks.ui.library.LibraryScreen
@@ -45,13 +44,11 @@ class LibrarySearchScreenshotTest {
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) },
artists = remember { flowOf(PagingData.empty<Artist>()) },
songs = remember { flowOf(PagingData.empty<SongListItem>()) },
playlists = remember { flowOf(PagingData.empty<Playlist>()) },
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
listQuery = ListQuery("Name"),
@@ -66,7 +66,7 @@ class QueueViewModelTest {
@Test
fun openingLoadsAWindowAtTheCursorAndExtendingGrowsIt() {
runBlocking { seedSongs(200) }
controller.playSongs(1, 100)
controller.playAlbum(1, "al1", 100)
await { controller.state.value.position == 100L }
viewModel.open()
@@ -87,7 +87,7 @@ class QueueViewModelTest {
@Test
fun openingAtTheEndLoadsAWindowAndExtendsOlder() {
runBlocking { seedSongs(200) }
controller.playSongs(1, 199)
controller.playAlbum(1, "al1", 199)
await { controller.state.value.position == 199L }
viewModel.open()
@@ -111,7 +111,7 @@ class QueueViewModelTest {
@Test
fun reopeningReusesRowIdentitySoComposeDoesNotCrossFadeTwoLists() {
runBlocking { seedSongs(200) }
controller.playSongs(1, 100)
controller.playAlbum(1, "al1", 100)
await { controller.state.value.position == 100L }
viewModel.open()
@@ -128,7 +128,7 @@ class QueueViewModelTest {
@Test
fun reorderingMovesTheRowLocally() {
runBlocking { seedSongs(200) }
controller.playSongs(1, 100)
controller.playAlbum(1, "al1", 100)
await { controller.state.value.position == 100L }
viewModel.open()