play the songs list from the songs tab

This commit is contained in:
austinried
2026-10-04 00:29:51 +00:00
parent 131b0d56a6
commit ebe76cd5bb
14 changed files with 124 additions and 17 deletions
@@ -21,6 +21,13 @@ 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"
@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface LibraryDao {
@@ -93,12 +100,7 @@ interface LibraryDao {
@Query("SELECT * FROM albums WHERE sourceId = :sourceId ORDER BY created DESC, name COLLATE NOCASE")
fun albumsByRecentlyAdded(sourceId: Long): PagingSource<Int, Album>
@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 " +
"ORDER BY songs.title COLLATE NOCASE, songs.artist COLLATE NOCASE",
)
@Query(SONGS_SQL)
fun songs(sourceId: Long): PagingSource<Int, SongListItem>
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId ORDER BY name COLLATE NOCASE")
@@ -47,6 +47,9 @@ 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,
@@ -67,6 +70,12 @@ interface QueueDao {
offset: Long,
): SongListItem?
@Query("$SONGS_SQL LIMIT 1 OFFSET :offset")
suspend fun songAt(
sourceId: Long,
offset: Long,
): SongListItem?
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
@@ -175,7 +175,7 @@ data class SearchIndex(
val title: String,
)
enum class QueueKind { Playlist, Album, Song }
enum class QueueKind { Playlist, Album, Song, Songs }
@Entity(
tableName = "queue_entries",
@@ -53,6 +53,8 @@ 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 = "")
suspend fun replace(entries: List<QueueEntry>) {
db.useWriterConnection { transactor ->
transactor.immediateTransaction {
@@ -99,6 +101,7 @@ class QueueRepository(
QueueKind.Playlist -> dao.playlistSongAt(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.Songs -> dao.songAt(entry.sourceId, index)
}
}
@@ -108,6 +111,7 @@ class QueueRepository(
QueueKind.Song -> dao.songLength(sourceId, refId)
QueueKind.Playlist -> dao.playlistLength(sourceId, refId)
QueueKind.Album -> dao.albumLength(sourceId, refId)
QueueKind.Songs -> dao.songsLength(sourceId)
}
val available = (total - offset).coerceAtLeast(0)
return count?.coerceAtMost(available) ?: available
@@ -34,7 +34,7 @@ fun appModule(
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
single { PlaybackController(get(), get(), get()) }
viewModel { RootViewModel(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
viewModel { SettingsViewModel(get(), get()) }
viewModel { AddSourceViewModel(get(), get()) }
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
@@ -118,6 +118,11 @@ class PlaybackController(
startPosition: Long,
) = play(listOf(queueRepository.playlistEntry(sourceId, playlistId)), startPosition)
fun playSongs(
sourceId: Long,
startPosition: Long,
) = play(listOf(queueRepository.songsEntry(sourceId)), startPosition)
fun togglePlayPause() {
val player = player ?: return
if (player.isEnded) {
@@ -93,6 +93,7 @@ fun LibraryRoute(
) {
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
LibraryScreen(
selectedTab = selectedTab,
onTabSelected = { selectedTab = it },
@@ -105,8 +106,10 @@ fun LibraryRoute(
coverArt = viewModel::coverArt,
onAlbumClick = onAlbumClick,
onPlaylistClick = onPlaylistClick,
onSongClick = viewModel::playSong,
onSync = viewModel::sync,
onOpenSettings = onOpenSettings,
playingSongId = playingSongId,
)
}
@@ -122,9 +125,11 @@ fun LibraryScreen(
coverArt: (String?, Boolean) -> CoverArtRef?,
onAlbumClick: (Album) -> Unit,
onPlaylistClick: (Playlist) -> Unit,
onSongClick: (Int) -> Unit,
onSync: () -> Unit,
onOpenSettings: () -> Unit,
syncing: Boolean = false,
playingSongId: String? = null,
bottomInset: Dp = 0.dp,
modifier: Modifier = Modifier,
) {
@@ -206,7 +211,7 @@ fun LibraryScreen(
when (LibraryTab.entries[page]) {
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
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)
}
}
@@ -8,12 +8,14 @@ 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.UserPreferences
import com.subtracks.data.repo.LibraryRepository
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.SharingStarted
@@ -22,12 +24,14 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@OptIn(ExperimentalCoroutinesApi::class)
class LibraryViewModel(
private val libraryRepository: LibraryRepository,
private val sourceRepository: SourceRepository,
private val syncManager: SyncManager,
private val playbackController: PlaybackController,
userPreferences: UserPreferences,
) : ViewModel() {
val albums: Flow<PagingData<Album>> =
@@ -60,10 +64,22 @@ class LibraryViewModel(
.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 coverArt(
coverArt: String?,
thumbnail: Boolean,
): 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()
}
@@ -1,5 +1,6 @@
package com.subtracks.ui.library
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -36,6 +37,8 @@ fun SongsContent(
items: LazyPagingItems<SongListItem>,
coverArt: (String?, Boolean) -> CoverArtRef?,
bottomInset: Dp,
onSongClick: (Int) -> Unit,
playingSongId: String? = null,
modifier: Modifier = Modifier,
) {
when {
@@ -55,7 +58,13 @@ fun SongsContent(
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)
SongRow(
song = item.song,
coverArtId = item.coverArt,
coverArt = coverArt,
isPlaying = item.song.id == playingSongId,
modifier = Modifier.clickable { onSongClick(index) },
)
}
}
}