Drop DetailedSong model

This commit is contained in:
vfsfitvnm 2022-10-29 16:43:17 +02:00
parent f3995b8c46
commit 33221746fc
17 changed files with 151 additions and 163 deletions

View file

@ -38,10 +38,10 @@ import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.SongWithContentLength
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.Event
import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Format
import it.vfsfitvnm.vimusic.models.Info
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.PlaylistPreview import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
@ -62,34 +62,34 @@ interface Database {
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID ASC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID ASC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByRowIdAsc(): Flow<List<DetailedSong>> fun songsByRowIdAsc(): Flow<List<Song>>
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByRowIdDesc(): Flow<List<DetailedSong>> fun songsByRowIdDesc(): Flow<List<Song>>
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title ASC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title ASC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByTitleAsc(): Flow<List<DetailedSong>> fun songsByTitleAsc(): Flow<List<Song>>
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title DESC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title DESC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByTitleDesc(): Flow<List<DetailedSong>> fun songsByTitleDesc(): Flow<List<Song>>
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs ASC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs ASC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByPlayTimeAsc(): Flow<List<DetailedSong>> fun songsByPlayTimeAsc(): Flow<List<Song>>
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs DESC") @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs DESC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun songsByPlayTimeDesc(): Flow<List<DetailedSong>> fun songsByPlayTimeDesc(): Flow<List<Song>>
fun songs(sortBy: SongSortBy, sortOrder: SortOrder): Flow<List<DetailedSong>> { fun songs(sortBy: SongSortBy, sortOrder: SortOrder): Flow<List<Song>> {
return when (sortBy) { return when (sortBy) {
SongSortBy.PlayTime -> when (sortOrder) { SongSortBy.PlayTime -> when (sortOrder) {
SortOrder.Ascending -> songsByPlayTimeAsc() SortOrder.Ascending -> songsByPlayTimeAsc()
@ -109,7 +109,7 @@ interface Database {
@Transaction @Transaction
@Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC") @Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun favorites(): Flow<List<DetailedSong>> fun favorites(): Flow<List<Song>>
@Query("SELECT * FROM QueuedMediaItem") @Query("SELECT * FROM QueuedMediaItem")
fun queue(): List<QueuedMediaItem> fun queue(): List<QueuedMediaItem>
@ -187,7 +187,7 @@ interface Database {
@Transaction @Transaction
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position") @Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun albumSongs(albumId: String): Flow<List<DetailedSong>> fun albumSongs(albumId: String): Flow<List<Song>>
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC") @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC")
fun albumsByTitleAsc(): Flow<List<Album>> fun albumsByTitleAsc(): Flow<List<Album>>
@ -281,15 +281,14 @@ interface Database {
@Transaction @Transaction
@Query("SELECT * FROM Song JOIN SongArtistMap ON Song.id = SongArtistMap.songId WHERE SongArtistMap.artistId = :artistId AND totalPlayTimeMs > 0 ORDER BY Song.ROWID DESC") @Query("SELECT * FROM Song JOIN SongArtistMap ON Song.id = SongArtistMap.songId WHERE SongArtistMap.artistId = :artistId AND totalPlayTimeMs > 0 ORDER BY Song.ROWID DESC")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun artistSongs(artistId: String): Flow<List<DetailedSong>> fun artistSongs(artistId: String): Flow<List<Song>>
@Query("SELECT * FROM Format WHERE songId = :songId") @Query("SELECT * FROM Format WHERE songId = :songId")
fun format(songId: String): Flow<Format?> fun format(songId: String): Flow<Format?>
@Transaction @Transaction
@Query("SELECT * FROM Song JOIN Format ON id = songId WHERE contentLength IS NOT NULL AND totalPlayTimeMs > 0 ORDER BY Song.ROWID DESC") @Query("SELECT Song.*, contentLength FROM Song JOIN Format ON id = songId WHERE contentLength IS NOT NULL AND totalPlayTimeMs > 0 ORDER BY Song.ROWID DESC")
@RewriteQueriesToDropUnusedColumns fun songsWithContentLength(): Flow<List<SongWithContentLength>>
fun songsWithContentLength(): Flow<List<DetailedSongWithContentLength>>
@Query(""" @Query("""
UPDATE SongPlaylistMap SET position = UPDATE SongPlaylistMap SET position =
@ -312,12 +311,18 @@ interface Database {
fun loudnessDb(songId: String): Flow<Float?> fun loudnessDb(songId: String): Flow<Float?>
@Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query") @Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query")
fun search(query: String): Flow<List<DetailedSong>> fun search(query: String): Flow<List<Song>>
@Query("SELECT albumId AS id, NULL AS name FROM SongAlbumMap WHERE songId = :songId")
fun songAlbumInfo(songId: String): Info
@Query("SELECT id, name FROM Artist LEFT JOIN SongArtistMap ON id = artistId WHERE songId = :songId")
fun songArtistInfo(songId: String): List<Info>
@Transaction @Transaction
@Query("SELECT Song.* FROM Event JOIN Song ON Song.id = songId GROUP BY songId ORDER BY SUM(CAST(playTime AS REAL) / (((:now - timestamp) / 86400000) + 1)) DESC LIMIT 1") @Query("SELECT Song.* FROM Event JOIN Song ON Song.id = songId GROUP BY songId ORDER BY SUM(CAST(playTime AS REAL) / (((:now - timestamp) / 86400000) + 1)) DESC LIMIT 1")
@RewriteQueriesToDropUnusedColumns @RewriteQueriesToDropUnusedColumns
fun trending(now: Long = System.currentTimeMillis()): Flow<DetailedSong?> fun trending(now: Long = System.currentTimeMillis()): Flow<Song?>
@Query("SELECT COUNT (*) FROM Event") @Query("SELECT COUNT (*) FROM Event")
fun eventsCount(): Flow<Int> fun eventsCount(): Flow<Int>

View file

@ -1,47 +0,0 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.Junction
import androidx.room.Relation
@Immutable
open class DetailedSong(
val id: String,
val title: String,
val artistsText: String? = null,
val durationText: String?,
val thumbnailUrl: String?,
val totalPlayTimeMs: Long = 0,
@Relation(
entity = SongAlbumMap::class,
entityColumn = "songId",
parentColumn = "id",
projection = ["albumId"]
)
val albumId: String?,
@Relation(
entity = Artist::class,
entityColumn = "id",
parentColumn = "id",
associateBy = Junction(
value = SongArtistMap::class,
parentColumn = "songId",
entityColumn = "artistId"
),
projection = ["id", "name"]
)
val artists: List<Info>?
) {
val formattedTotalPlayTime: String
get() {
val seconds = totalPlayTimeMs / 1000
val hours = seconds / 3600
return when {
hours == 0L -> "${seconds / 60}m"
hours < 24L -> "${hours}h"
else -> "${hours / 24}d"
}
}
}

View file

@ -1,23 +0,0 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.Relation
@Immutable
class DetailedSongWithContentLength(
id: String,
title: String,
artistsText: String? = null,
durationText: String?,
thumbnailUrl: String?,
totalPlayTimeMs: Long = 0,
albumId: String?,
artists: List<Info>?,
@Relation(
entity = Format::class,
entityColumn = "songId",
parentColumn = "id",
projection = ["contentLength"]
)
val contentLength: Long?
) : DetailedSong(id, title, artistsText, durationText, thumbnailUrl, totalPlayTimeMs, albumId, artists)

View file

@ -18,5 +18,5 @@ data class PlaylistWithSongs(
entityColumn = "songId" entityColumn = "songId"
) )
) )
val songs: List<DetailedSong> val songs: List<Song>
) )

View file

@ -17,6 +17,19 @@ data class Song(
val likedAt: Long? = null, val likedAt: Long? = null,
val totalPlayTimeMs: Long = 0 val totalPlayTimeMs: Long = 0
) { ) {
val formattedTotalPlayTime: String
get() {
val seconds = totalPlayTimeMs / 1000
val hours = seconds / 3600
return when {
hours == 0L -> "${seconds / 60}m"
hours < 24L -> "${hours}h"
else -> "${hours / 24}d"
}
}
fun toggleLike(): Song { fun toggleLike(): Song {
return copy( return copy(
likedAt = if (likedAt == null) System.currentTimeMillis() else null likedAt = if (likedAt == null) System.currentTimeMillis() else null

View file

@ -0,0 +1,10 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.Embedded
@Immutable
data class SongWithContentLength(
@Embedded val song: Song,
val contentLength: Long?
)

View file

@ -20,8 +20,9 @@ import androidx.media3.datasource.cache.Cache
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.PlaylistPreview import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongWithContentLength
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forceSeekToNext import it.vfsfitvnm.vimusic.utils.forceSeekToNext
@ -36,7 +37,7 @@ import kotlinx.coroutines.withContext
class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection { class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
private val coroutineScope = CoroutineScope(Dispatchers.IO) private val coroutineScope = CoroutineScope(Dispatchers.IO)
private var lastSongs = emptyList<DetailedSong>() private var lastSongs = emptyList<Song>()
private var bound = false private var bound = false
@ -187,7 +188,7 @@ class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
BrowserMediaItem.FLAG_PLAYABLE BrowserMediaItem.FLAG_PLAYABLE
) )
private val DetailedSong.asBrowserMediaItem private val Song.asBrowserMediaItem
inline get() = BrowserMediaItem( inline get() = BrowserMediaItem(
BrowserMediaDescription.Builder() BrowserMediaDescription.Builder()
.setMediaId(MediaId.forSong(id)) .setMediaId(MediaId.forSong(id))
@ -254,9 +255,10 @@ class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
.first() .first()
.filter { song -> .filter { song ->
song.contentLength?.let { song.contentLength?.let {
cache.isCached(song.id, 0, song.contentLength) cache.isCached(song.song.id, 0, it)
} ?: false } ?: false
} }
.map(SongWithContentLength::song)
.shuffled() .shuffled()
MediaId.playlists -> data MediaId.playlists -> data
@ -273,7 +275,7 @@ class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
?.first() ?.first()
else -> emptyList() else -> emptyList()
}?.map(DetailedSong::asMediaItem) ?: return@launch }?.map(Song::asMediaItem) ?: return@launch
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
player.forcePlayAtIndex(mediaItems, index.coerceIn(0, mediaItems.size)) player.forcePlayAtIndex(mediaItems, index.coerceIn(0, mediaItems.size))

View file

@ -25,6 +25,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -48,7 +49,7 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Info
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
@ -69,15 +70,16 @@ import it.vfsfitvnm.vimusic.utils.formatAsDuration
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun InHistoryMediaItemMenu( fun InHistoryMediaItemMenu(
onDismiss: () -> Unit, onDismiss: () -> Unit,
song: DetailedSong, song: Song,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@ -115,7 +117,7 @@ fun InPlaylistMediaItemMenu(
onDismiss: () -> Unit, onDismiss: () -> Unit,
playlistId: Long, playlistId: Long,
positionInPlaylist: Int, positionInPlaylist: Int,
song: DetailedSong, song: Song,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
NonQueuedMediaItemMenu( NonQueuedMediaItemMenu(
@ -276,15 +278,43 @@ fun MediaItemMenu(
mutableStateOf(0.dp) mutableStateOf(0.dp)
} }
val likedAt by remember(mediaItem.mediaId) { var albumInfo by remember {
Database.likedAt(mediaItem.mediaId).distinctUntilChanged() mutableStateOf(mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
}.collectAsState(initial = null, context = Dispatchers.IO) Info(albumId, null)
})
}
var artistsInfo by remember {
mutableStateOf(
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames ->
mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds")?.let { artistIds ->
artistNames.zip(artistIds).map { (authorName, authorId) ->
Info(authorId, authorName)
}
}
}
)
}
var likedAt by remember {
mutableStateOf<Long?>(null)
}
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
if (albumInfo == null) albumInfo = Database.songAlbumInfo(mediaItem.mediaId)
if (artistsInfo == null) artistsInfo = Database.songArtistInfo(mediaItem.mediaId)
Database.likedAt(mediaItem.mediaId).collect { likedAt = it }
}
}
AnimatedContent( AnimatedContent(
targetState = isViewingPlaylists, targetState = isViewingPlaylists,
transitionSpec = { transitionSpec = {
val animationSpec = tween<IntOffset>(400) val animationSpec = tween<IntOffset>(400)
val slideDirection = if (targetState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right val slideDirection =
if (targetState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
slideIntoContainer(slideDirection, animationSpec) with slideIntoContainer(slideDirection, animationSpec) with
slideOutOfContainer(slideDirection, animationSpec) slideOutOfContainer(slideDirection, animationSpec)
@ -371,7 +401,8 @@ fun MediaItemMenu(
.padding(end = 12.dp) .padding(end = 12.dp)
) { ) {
SongItem( SongItem(
thumbnailUrl = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx)?.toString(), thumbnailUrl = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx)
?.toString(),
title = mediaItem.mediaMetadata.title.toString(), title = mediaItem.mediaMetadata.title.toString(),
authors = mediaItem.mediaMetadata.artist.toString(), authors = mediaItem.mediaMetadata.artist.toString(),
duration = null, duration = null,
@ -601,7 +632,10 @@ fun MediaItemMenu(
text = "${formatAsDuration(it)} left", text = "${formatAsDuration(it)} left",
style = typography.xxs.medium, style = typography.xxs.medium,
modifier = modifier modifier = modifier
.background(color = colorPalette.background0, shape = RoundedCornerShape(16.dp)) .background(
color = colorPalette.background0,
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.animateContentSize() .animateContentSize()
) )
@ -619,7 +653,9 @@ fun MediaItemMenu(
Image( Image(
painter = painterResource(R.drawable.chevron_forward), painter = painterResource(R.drawable.chevron_forward),
contentDescription = null, contentDescription = null,
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(colorPalette.textSecondary), colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
colorPalette.textSecondary
),
modifier = Modifier modifier = Modifier
.size(16.dp) .size(16.dp)
) )
@ -628,7 +664,7 @@ fun MediaItemMenu(
} }
onGoToAlbum?.let { onGoToAlbum -> onGoToAlbum?.let { onGoToAlbum ->
mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> albumInfo?.let { (albumId) ->
MenuEntry( MenuEntry(
icon = R.drawable.disc, icon = R.drawable.disc,
text = "Go to album", text = "Go to album",
@ -641,25 +677,16 @@ fun MediaItemMenu(
} }
onGoToArtist?.let { onGoToArtist -> onGoToArtist?.let { onGoToArtist ->
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames") artistsInfo?.forEach { (authorId, authorName) ->
?.let { artistNames -> MenuEntry(
mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds") icon = R.drawable.person,
?.let { artistIds -> text = "More of $authorName",
artistNames.zip(artistIds) onClick = {
.forEach { (authorName, authorId) -> onDismiss()
if (authorId != null) { onGoToArtist(authorId)
MenuEntry( }
icon = R.drawable.person, )
text = "More of $authorName", }
onClick = {
onDismiss()
onGoToArtist(authorId)
}
)
}
}
}
}
} }
onRemoveFromQueue?.let { onRemoveFromQueue -> onRemoveFromQueue?.let { onRemoveFromQueue ->

View file

@ -19,7 +19,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import coil.compose.AsyncImage import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.ui.styling.shimmer
@ -28,6 +27,7 @@ import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.innertube.Innertube import it.vfsfitvnm.innertube.Innertube
import it.vfsfitvnm.vimusic.models.Song
@Composable @Composable
fun SongItem( fun SongItem(
@ -69,7 +69,7 @@ fun SongItem(
@Composable @Composable
fun SongItem( fun SongItem(
song: DetailedSong, song: Song,
thumbnailSizePx: Int, thumbnailSizePx: Int,
thumbnailSizeDp: Dp, thumbnailSizeDp: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View file

@ -27,7 +27,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
@ -59,7 +59,7 @@ fun AlbumSongs(
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
var songs by persistList<DetailedSong>("album/$browseId/songs") var songs by persistList<Song>("album/$browseId/songs")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Database.albumSongs(browseId).collect { songs = it } Database.albumSongs(browseId).collect { songs = it }
@ -89,7 +89,7 @@ fun AlbumSongs(
text = "Enqueue", text = "Enqueue",
enabled = songs.isNotEmpty(), enabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs.map(Song::asMediaItem))
} }
) )
} }
@ -133,7 +133,7 @@ fun AlbumSongs(
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex( binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem), songs.map(Song::asMediaItem),
index index
) )
} }
@ -162,7 +162,7 @@ fun AlbumSongs(
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem) songs.shuffled().map(Song::asMediaItem)
) )
} }
} }

View file

@ -24,7 +24,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
@ -53,7 +53,7 @@ fun ArtistLocalSongs(
val (colorPalette) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
var songs by persist<List<DetailedSong>?>("artist/$browseId/localSongs") var songs by persist<List<Song>?>("artist/$browseId/localSongs")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Database.artistSongs(browseId).collect { songs = it } Database.artistSongs(browseId).collect { songs = it }
@ -84,7 +84,7 @@ fun ArtistLocalSongs(
text = "Enqueue", text = "Enqueue",
enabled = !songs.isNullOrEmpty(), enabled = !songs.isNullOrEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs!!.map(Song::asMediaItem))
} }
) )
} }
@ -115,7 +115,7 @@ fun ArtistLocalSongs(
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex( binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem), songs.map(Song::asMediaItem),
index index
) )
} }
@ -139,7 +139,7 @@ fun ArtistLocalSongs(
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem) songs.shuffled().map(Song::asMediaItem)
) )
} }
} }

View file

@ -26,7 +26,8 @@ import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongWithContentLength
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -53,7 +54,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
var songs by persistList<DetailedSong>("${builtInPlaylist.name}/songs") var songs by persistList<Song>("${builtInPlaylist.name}/songs")
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
when (builtInPlaylist) { when (builtInPlaylist) {
@ -66,9 +67,9 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
.map { songs -> .map { songs ->
songs.filter { song -> songs.filter { song ->
song.contentLength?.let { song.contentLength?.let {
binder?.cache?.isCached(song.id, 0, song.contentLength) binder?.cache?.isCached(song.song.id, 0, song.contentLength)
} ?: false } ?: false
} }.map(SongWithContentLength::song)
} }
}.collect { songs = it } }.collect { songs = it }
} }
@ -103,7 +104,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
text = "Enqueue", text = "Enqueue",
enabled = songs.isNotEmpty(), enabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs.map(Song::asMediaItem))
} }
) )
@ -143,7 +144,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex( binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem), songs.map(Song::asMediaItem),
index index
) )
} }
@ -160,7 +161,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem) songs.shuffled().map(Song::asMediaItem)
) )
} }
} }

View file

@ -38,7 +38,8 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -58,6 +59,8 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.songSortByKey import it.vfsfitvnm.vimusic.utils.songSortByKey
import it.vfsfitvnm.vimusic.utils.songSortOrderKey import it.vfsfitvnm.vimusic.utils.songSortOrderKey
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.flow.first
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ -75,9 +78,12 @@ fun HomeSongs(
var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded) var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending) var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
var items by persistList<DetailedSong>("home/songs") var items by persistList<Song>("home/songs")
LaunchedEffect(sortBy, sortOrder) { LaunchedEffect(sortBy, sortOrder) {
// 670, 58, 97, 91, 94
println("took ${measureTimeMillis { Database.songs(sortBy, sortOrder).first() }}ms")
Database.songs(sortBy, sortOrder).collect { items = it } Database.songs(sortBy, sortOrder).collect { items = it }
} }
@ -175,7 +181,7 @@ fun HomeSongs(
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex( binder?.player?.forcePlayAtIndex(
items.map(DetailedSong::asMediaItem), items.map(Song::asMediaItem),
index index
) )
} }

View file

@ -47,7 +47,7 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
@ -89,7 +89,7 @@ fun QuickPicks(
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
val windowInsets = LocalPlayerAwareWindowInsets.current val windowInsets = LocalPlayerAwareWindowInsets.current
var trending by persist<DetailedSong?>("home/trending") var trending by persist<Song?>("home/trending")
var relatedPageResult by persist<Result<Innertube.RelatedPage?>?>(tag = "home/relatedPageResult") var relatedPageResult by persist<Result<Innertube.RelatedPage?>?>(tag = "home/relatedPageResult")

View file

@ -36,8 +36,8 @@ import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.transaction
@ -158,7 +158,7 @@ fun LocalPlaylistSongs(
enabled = playlistWithSongs?.songs?.isNotEmpty() == true, enabled = playlistWithSongs?.songs?.isNotEmpty() == true,
onClick = { onClick = {
playlistWithSongs?.songs playlistWithSongs?.songs
?.map(DetailedSong::asMediaItem) ?.map(Song::asMediaItem)
?.let { mediaItems -> ?.let { mediaItems ->
binder?.player?.enqueue(mediaItems) binder?.player?.enqueue(mediaItems)
} }
@ -266,7 +266,7 @@ fun LocalPlaylistSongs(
}, },
onClick = { onClick = {
playlistWithSongs?.songs playlistWithSongs?.songs
?.map(DetailedSong::asMediaItem) ?.map(Song::asMediaItem)
?.let { mediaItems -> ?.let { mediaItems ->
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index) binder?.player?.forcePlayAtIndex(mediaItems, index)
@ -288,7 +288,7 @@ fun LocalPlaylistSongs(
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem) songs.shuffled().map(Song::asMediaItem)
) )
} }
} }

View file

@ -27,7 +27,7 @@ import it.vfsfitvnm.innertube.models.NavigationEndpoint
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
@ -54,7 +54,7 @@ fun LocalSongSearch(
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
var items by persistList<DetailedSong>("search/local/songs") var items by persistList<Song>("search/local/songs")
LaunchedEffect(textFieldValue.text) { LaunchedEffect(textFieldValue.text) {
if (textFieldValue.text.length > 1) { if (textFieldValue.text.length > 1) {
@ -105,7 +105,7 @@ fun LocalSongSearch(
items( items(
items = items, items = items,
key = DetailedSong::id, key = Song::id,
) { song -> ) { song ->
SongItem( SongItem(
song = song, song = song,

View file

@ -7,11 +7,11 @@ import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.innertube.Innertube import it.vfsfitvnm.innertube.Innertube
import it.vfsfitvnm.innertube.models.bodies.ContinuationBody import it.vfsfitvnm.innertube.models.bodies.ContinuationBody
import it.vfsfitvnm.innertube.requests.playlistPage import it.vfsfitvnm.innertube.requests.playlistPage
import it.vfsfitvnm.innertube.utils.plus import it.vfsfitvnm.innertube.utils.plus
import it.vfsfitvnm.vimusic.models.Song
val Innertube.SongItem.asMediaItem: MediaItem val Innertube.SongItem.asMediaItem: MediaItem
get() = MediaItem.Builder() get() = MediaItem.Builder()
@ -26,7 +26,6 @@ val Innertube.SongItem.asMediaItem: MediaItem
.setArtworkUri(thumbnail?.url?.toUri()) .setArtworkUri(thumbnail?.url?.toUri())
.setExtras( .setExtras(
bundleOf( bundleOf(
"videoId" to key,
"albumId" to album?.endpoint?.browseId, "albumId" to album?.endpoint?.browseId,
"durationText" to durationText, "durationText" to durationText,
"artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name }, "artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
@ -49,7 +48,6 @@ val Innertube.VideoItem.asMediaItem: MediaItem
.setArtworkUri(thumbnail?.url?.toUri()) .setArtworkUri(thumbnail?.url?.toUri())
.setExtras( .setExtras(
bundleOf( bundleOf(
"videoId" to key,
"durationText" to durationText, "durationText" to durationText,
"artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.mapNotNull { it.name } else null, "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.mapNotNull { it.name } else null,
"artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null, "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null,
@ -59,7 +57,7 @@ val Innertube.VideoItem.asMediaItem: MediaItem
) )
.build() .build()
val DetailedSong.asMediaItem: MediaItem val Song.asMediaItem: MediaItem
get() = MediaItem.Builder() get() = MediaItem.Builder()
.setMediaMetadata( .setMediaMetadata(
MediaMetadata.Builder() MediaMetadata.Builder()
@ -68,10 +66,6 @@ val DetailedSong.asMediaItem: MediaItem
.setArtworkUri(thumbnailUrl?.toUri()) .setArtworkUri(thumbnailUrl?.toUri())
.setExtras( .setExtras(
bundleOf( bundleOf(
"videoId" to id,
"albumId" to albumId,
"artistNames" to artists?.map { it.name },
"artistIds" to artists?.map { it.id },
"durationText" to durationText "durationText" to durationText
) )
) )