mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
play the songs list from the songs tab
This commit is contained in:
@@ -21,6 +21,13 @@ internal const val PLAYLIST_SONGS_SQL =
|
|||||||
"WHERE playlist_songs.sourceId = :sourceId AND playlist_songs.playlistId = :playlistId " +
|
"WHERE playlist_songs.sourceId = :sourceId AND playlist_songs.playlistId = :playlistId " +
|
||||||
"ORDER BY playlist_songs.position"
|
"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"
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
|
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
|
||||||
interface LibraryDao {
|
interface LibraryDao {
|
||||||
@@ -93,12 +100,7 @@ interface LibraryDao {
|
|||||||
@Query("SELECT * FROM albums WHERE sourceId = :sourceId ORDER BY created DESC, name COLLATE NOCASE")
|
@Query("SELECT * FROM albums WHERE sourceId = :sourceId ORDER BY created DESC, name COLLATE NOCASE")
|
||||||
fun albumsByRecentlyAdded(sourceId: Long): PagingSource<Int, Album>
|
fun albumsByRecentlyAdded(sourceId: Long): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(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 songs.title COLLATE NOCASE, songs.artist COLLATE NOCASE",
|
|
||||||
)
|
|
||||||
fun songs(sourceId: Long): PagingSource<Int, SongListItem>
|
fun songs(sourceId: Long): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId ORDER BY name COLLATE NOCASE")
|
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId ORDER BY name COLLATE NOCASE")
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ interface QueueDao {
|
|||||||
albumId: String,
|
albumId: String,
|
||||||
): Long
|
): 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")
|
@Query("SELECT COUNT(*) FROM songs WHERE sourceId = :sourceId AND id = :songId")
|
||||||
suspend fun songLength(
|
suspend fun songLength(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
@@ -67,6 +70,12 @@ interface QueueDao {
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
): SongListItem?
|
): SongListItem?
|
||||||
|
|
||||||
|
@Query("$SONGS_SQL LIMIT 1 OFFSET :offset")
|
||||||
|
suspend fun songAt(
|
||||||
|
sourceId: Long,
|
||||||
|
offset: Long,
|
||||||
|
): SongListItem?
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ data class SearchIndex(
|
|||||||
val title: String,
|
val title: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class QueueKind { Playlist, Album, Song }
|
enum class QueueKind { Playlist, Album, Song, Songs }
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "queue_entries",
|
tableName = "queue_entries",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class QueueRepository(
|
|||||||
songId: String,
|
songId: String,
|
||||||
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = songId)
|
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = songId)
|
||||||
|
|
||||||
|
fun songsEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Songs, refId = "")
|
||||||
|
|
||||||
suspend fun replace(entries: List<QueueEntry>) {
|
suspend fun replace(entries: List<QueueEntry>) {
|
||||||
db.useWriterConnection { transactor ->
|
db.useWriterConnection { transactor ->
|
||||||
transactor.immediateTransaction {
|
transactor.immediateTransaction {
|
||||||
@@ -99,6 +101,7 @@ class QueueRepository(
|
|||||||
QueueKind.Playlist -> dao.playlistSongAt(entry.sourceId, entry.refId, index)
|
QueueKind.Playlist -> dao.playlistSongAt(entry.sourceId, entry.refId, index)
|
||||||
QueueKind.Album -> dao.albumSongAt(entry.sourceId, entry.refId, index)
|
QueueKind.Album -> dao.albumSongAt(entry.sourceId, entry.refId, index)
|
||||||
QueueKind.Song -> if (offset == 0L) dao.song(entry.sourceId, entry.refId) else null
|
QueueKind.Song -> if (offset == 0L) dao.song(entry.sourceId, entry.refId) else null
|
||||||
|
QueueKind.Songs -> dao.songAt(entry.sourceId, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +111,7 @@ class QueueRepository(
|
|||||||
QueueKind.Song -> dao.songLength(sourceId, refId)
|
QueueKind.Song -> dao.songLength(sourceId, refId)
|
||||||
QueueKind.Playlist -> dao.playlistLength(sourceId, refId)
|
QueueKind.Playlist -> dao.playlistLength(sourceId, refId)
|
||||||
QueueKind.Album -> dao.albumLength(sourceId, refId)
|
QueueKind.Album -> dao.albumLength(sourceId, refId)
|
||||||
|
QueueKind.Songs -> dao.songsLength(sourceId)
|
||||||
}
|
}
|
||||||
val available = (total - offset).coerceAtLeast(0)
|
val available = (total - offset).coerceAtLeast(0)
|
||||||
return count?.coerceAtMost(available) ?: available
|
return count?.coerceAtMost(available) ?: available
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun appModule(
|
|||||||
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
|
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
|
||||||
single { PlaybackController(get(), get(), get()) }
|
single { PlaybackController(get(), get(), get()) }
|
||||||
viewModel { RootViewModel(get()) }
|
viewModel { RootViewModel(get()) }
|
||||||
viewModel { LibraryViewModel(get(), get(), get(), get()) }
|
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
|
||||||
viewModel { SettingsViewModel(get(), get()) }
|
viewModel { SettingsViewModel(get(), get()) }
|
||||||
viewModel { AddSourceViewModel(get(), get()) }
|
viewModel { AddSourceViewModel(get(), get()) }
|
||||||
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
|
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ class PlaybackController(
|
|||||||
startPosition: Long,
|
startPosition: Long,
|
||||||
) = play(listOf(queueRepository.playlistEntry(sourceId, playlistId)), startPosition)
|
) = play(listOf(queueRepository.playlistEntry(sourceId, playlistId)), startPosition)
|
||||||
|
|
||||||
|
fun playSongs(
|
||||||
|
sourceId: Long,
|
||||||
|
startPosition: Long,
|
||||||
|
) = play(listOf(queueRepository.songsEntry(sourceId)), startPosition)
|
||||||
|
|
||||||
fun togglePlayPause() {
|
fun togglePlayPause() {
|
||||||
val player = player ?: return
|
val player = player ?: return
|
||||||
if (player.isEnded) {
|
if (player.isEnded) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ fun LibraryRoute(
|
|||||||
) {
|
) {
|
||||||
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
||||||
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
||||||
|
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
|
||||||
LibraryScreen(
|
LibraryScreen(
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = { selectedTab = it },
|
||||||
@@ -105,8 +106,10 @@ fun LibraryRoute(
|
|||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
onPlaylistClick = onPlaylistClick,
|
onPlaylistClick = onPlaylistClick,
|
||||||
|
onSongClick = viewModel::playSong,
|
||||||
onSync = viewModel::sync,
|
onSync = viewModel::sync,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
|
playingSongId = playingSongId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +125,11 @@ fun LibraryScreen(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
|
onSongClick: (Int) -> Unit,
|
||||||
onSync: () -> Unit,
|
onSync: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
syncing: Boolean = false,
|
syncing: Boolean = false,
|
||||||
|
playingSongId: String? = null,
|
||||||
bottomInset: Dp = 0.dp,
|
bottomInset: Dp = 0.dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -206,7 +211,7 @@ fun LibraryScreen(
|
|||||||
when (LibraryTab.entries[page]) {
|
when (LibraryTab.entries[page]) {
|
||||||
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
||||||
LibraryTab.Artists -> ArtistsContent(artists, coverArt, bottomInset)
|
LibraryTab.Artists -> ArtistsContent(artists, coverArt, bottomInset)
|
||||||
LibraryTab.Songs -> SongsContent(songs, coverArt, bottomInset)
|
LibraryTab.Songs -> SongsContent(songs, coverArt, bottomInset, onSongClick, playingSongId)
|
||||||
LibraryTab.Playlists -> PlaylistsContent(playlists, coverArt, bottomInset, onPlaylistClick)
|
LibraryTab.Playlists -> PlaylistsContent(playlists, coverArt, bottomInset, onPlaylistClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import com.subtracks.data.model.Album
|
|||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
|
import com.subtracks.data.model.QueueKind
|
||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.data.prefs.UserPreferences
|
import com.subtracks.data.prefs.UserPreferences
|
||||||
import com.subtracks.data.repo.LibraryRepository
|
import com.subtracks.data.repo.LibraryRepository
|
||||||
import com.subtracks.data.repo.SourceRepository
|
import com.subtracks.data.repo.SourceRepository
|
||||||
import com.subtracks.data.sync.SyncManager
|
import com.subtracks.data.sync.SyncManager
|
||||||
import com.subtracks.data.sync.SyncStatus
|
import com.subtracks.data.sync.SyncStatus
|
||||||
|
import com.subtracks.playback.PlaybackController
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -22,12 +24,14 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class LibraryViewModel(
|
class LibraryViewModel(
|
||||||
private val libraryRepository: LibraryRepository,
|
private val libraryRepository: LibraryRepository,
|
||||||
private val sourceRepository: SourceRepository,
|
private val sourceRepository: SourceRepository,
|
||||||
private val syncManager: SyncManager,
|
private val syncManager: SyncManager,
|
||||||
|
private val playbackController: PlaybackController,
|
||||||
userPreferences: UserPreferences,
|
userPreferences: UserPreferences,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val albums: Flow<PagingData<Album>> =
|
val albums: Flow<PagingData<Album>> =
|
||||||
@@ -60,10 +64,22 @@ class LibraryViewModel(
|
|||||||
.map { it == SyncStatus.Running }
|
.map { it == SyncStatus.Running }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
.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 coverArt(
|
fun coverArt(
|
||||||
coverArt: String?,
|
coverArt: String?,
|
||||||
thumbnail: Boolean,
|
thumbnail: Boolean,
|
||||||
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
|
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
|
||||||
|
|
||||||
|
fun playSong(position: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val sourceId = sourceRepository.activeSourceIdOnce() ?: return@launch
|
||||||
|
playbackController.playSongs(sourceId, position.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sync() = syncManager.requestSync()
|
fun sync() = syncManager.requestSync()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.subtracks.ui.library
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -36,6 +37,8 @@ fun SongsContent(
|
|||||||
items: LazyPagingItems<SongListItem>,
|
items: LazyPagingItems<SongListItem>,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
|
onSongClick: (Int) -> Unit,
|
||||||
|
playingSongId: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
@@ -55,7 +58,13 @@ fun SongsContent(
|
|||||||
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
|
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
|
||||||
val item = items[index]
|
val item = items[index]
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
SongRow(song = item.song, coverArtId = item.coverArt, coverArt = coverArt)
|
SongRow(
|
||||||
|
song = item.song,
|
||||||
|
coverArtId = item.coverArt,
|
||||||
|
coverArt = coverArt,
|
||||||
|
isPlaying = item.song.id == playingSongId,
|
||||||
|
modifier = Modifier.clickable { onSongClick(index) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
|||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.subtracks.data.db.SubtracksDatabase
|
import com.subtracks.data.db.SubtracksDatabase
|
||||||
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.PlaylistSong
|
import com.subtracks.data.model.PlaylistSong
|
||||||
import com.subtracks.data.model.QueueEntry
|
import com.subtracks.data.model.QueueEntry
|
||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
@@ -70,6 +71,19 @@ class QueueRepositoryTest {
|
|||||||
assertEquals(listOf("s2", "s3"), resolveAll(snapshot))
|
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
|
@Test
|
||||||
fun windowSpansEntryBoundaries() =
|
fun windowSpansEntryBoundaries() =
|
||||||
runTest {
|
runTest {
|
||||||
@@ -226,28 +240,55 @@ class QueueRepositoryTest {
|
|||||||
db.sourcesDao().upsertSource(
|
db.sourcesDao().upsertSource(
|
||||||
Source(id = 1, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
|
Source(id = 1, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
|
||||||
)
|
)
|
||||||
|
db.libraryDao().upsertAlbums(
|
||||||
|
listOf(
|
||||||
|
album("al1", "First Album", "Artist One"),
|
||||||
|
album("al2", "Second Album", "Artist Two"),
|
||||||
|
),
|
||||||
|
)
|
||||||
db.libraryDao().upsertSongs(
|
db.libraryDao().upsertSongs(
|
||||||
listOf(
|
listOf(
|
||||||
song("s1", "al1", track = 1),
|
song("s1", "al1", track = 1, album = "First Album"),
|
||||||
song("s2", "al1", track = 2),
|
song("s2", "al1", track = 2, album = "First Album"),
|
||||||
song("s3", "al1", track = 3),
|
song("s3", "al1", track = 3, album = "First Album"),
|
||||||
song("s4", "al2", track = 1),
|
song("s4", "al2", track = 1, album = "Second Album"),
|
||||||
song("s5", "al2", track = 2),
|
song("s5", "al2", track = 2, album = "Second Album"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun album(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
albumArtist: String,
|
||||||
|
) = Album(
|
||||||
|
sourceId = 1,
|
||||||
|
id = id,
|
||||||
|
artistId = "ar1",
|
||||||
|
name = name,
|
||||||
|
albumArtist = albumArtist,
|
||||||
|
created = 0,
|
||||||
|
coverArt = null,
|
||||||
|
genre = null,
|
||||||
|
year = null,
|
||||||
|
starred = null,
|
||||||
|
songCount = 1,
|
||||||
|
frequentRank = null,
|
||||||
|
recentRank = null,
|
||||||
|
)
|
||||||
|
|
||||||
private fun song(
|
private fun song(
|
||||||
id: String,
|
id: String,
|
||||||
albumId: String,
|
albumId: String,
|
||||||
track: Long,
|
track: Long,
|
||||||
|
album: String = "Album",
|
||||||
) = Song(
|
) = Song(
|
||||||
sourceId = 1,
|
sourceId = 1,
|
||||||
id = id,
|
id = id,
|
||||||
albumId = albumId,
|
albumId = albumId,
|
||||||
artistId = "ar1",
|
artistId = "ar1",
|
||||||
title = "Song $id",
|
title = "Song $id",
|
||||||
album = "Album",
|
album = album,
|
||||||
artist = "Artist",
|
artist = "Artist",
|
||||||
duration = 100,
|
duration = 100,
|
||||||
track = track,
|
track = track,
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ class PlaybackControllerTest {
|
|||||||
assertEquals(listOf("prepare", "play"), handle.operations.takeLast(2))
|
assertEquals(listOf("prepare", "play"), handle.operations.takeLast(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playingTheSongsListQueuesTheWholeLibrary() {
|
||||||
|
seedAlbum(60, sourceId = 1)
|
||||||
|
|
||||||
|
controller.playSongs(1, 30)
|
||||||
|
await { handle.operations.contains("play") }
|
||||||
|
|
||||||
|
assertEquals(51, handle.itemCount)
|
||||||
|
assertEquals(QueueContext(QueueKind.Songs, ""), controller.state.value.context)
|
||||||
|
assertTrue(controller.state.value.hasNext)
|
||||||
|
assertTrue(controller.state.value.hasPrevious)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
||||||
seedAlbum(100, sourceId = 1)
|
seedAlbum(100, sourceId = 1)
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ class AppScreenshotTest {
|
|||||||
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
onAlbumClick = {},
|
onAlbumClick = {},
|
||||||
onPlaylistClick = {},
|
onPlaylistClick = {},
|
||||||
|
onSongClick = {},
|
||||||
onSync = {},
|
onSync = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class LibraryScreenTest {
|
|||||||
coverArt = { _, _ -> null },
|
coverArt = { _, _ -> null },
|
||||||
onAlbumClick = {},
|
onAlbumClick = {},
|
||||||
onPlaylistClick = {},
|
onPlaylistClick = {},
|
||||||
|
onSongClick = {},
|
||||||
onSync = {},
|
onSync = {},
|
||||||
onOpenSettings = {},
|
onOpenSettings = {},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ Not done yet in this slice (deliberately): artist detail, cover-art tonal colour
|
|||||||
- (done) Media notifications and headset/Bluetooth controls, from the media session.
|
- (done) Media notifications and headset/Bluetooth controls, from the media session.
|
||||||
- (done) A SQL-backed queue of references (whole playlist / album / song, each with an optional ordinal range) resolved lazily, with only a bounded window in memory and in the player.
|
- (done) A SQL-backed queue of references (whole playlist / album / song, each with an optional ordinal range) resolved lazily, with only a bounded window in memory and in the player.
|
||||||
- (done) Streaming bitrate and preferred format from settings (`maxBitRate`/`format`, so the server transcodes as configured).
|
- (done) Streaming bitrate and preferred format from settings (`maxBitRate`/`format`, so the server transcodes as configured).
|
||||||
- Playing from the Songs tab, the queue view (add to queue / play next / reorder), and gapless format preferences.
|
- (done) Playing from the Songs tab: the tapped song queues the whole songs list at that position.
|
||||||
|
- The queue view (add to queue / play next / reorder), and gapless format preferences.
|
||||||
- Scrobbling and "now playing" back to the server.
|
- Scrobbling and "now playing" back to the server.
|
||||||
- Optional download/caching.
|
- Optional download/caching.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user