diff --git a/core/ui/src/main/java/ru/stersh/youamp/core/ui/PlayAllButton.kt b/core/ui/src/main/java/ru/stersh/youamp/core/ui/PlayAllButton.kt new file mode 100644 index 00000000..03ce8a00 --- /dev/null +++ b/core/ui/src/main/java/ru/stersh/youamp/core/ui/PlayAllButton.kt @@ -0,0 +1,38 @@ +package ru.stersh.youamp.core.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun PlayAllFabButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ExtendedFloatingActionButton( + onClick = onClick, + modifier = modifier + ) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = stringResource(R.string.play_all_title) + ) + Text(text = stringResource(R.string.play_all_title)) + } +} + +@Composable +@Preview +private fun PlayAllFabButtonPreview() { + YouampPlayerTheme { + PlayAllFabButton( + onClick = {} + ) + } +} \ No newline at end of file diff --git a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/Di.kt b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/Di.kt index bd914d34..5e53ffbf 100644 --- a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/Di.kt +++ b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/Di.kt @@ -8,5 +8,5 @@ import ru.stresh.youamp.feature.favorite.list.ui.FavoriteListViewModel val favoriteListModule = module { single { FavoritesRepositoryImpl(get()) } - viewModel { FavoriteListViewModel(get()) } + viewModel { FavoriteListViewModel(get(), get(), get()) } } \ No newline at end of file diff --git a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/data/FavoritesRepositoryImpl.kt b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/data/FavoritesRepositoryImpl.kt index efd15998..c65cba44 100644 --- a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/data/FavoritesRepositoryImpl.kt +++ b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/data/FavoritesRepositoryImpl.kt @@ -30,7 +30,8 @@ internal class FavoritesRepositoryImpl(private val apiProvider: ApiProvider) : F id = id, title = title, artist = artist, - artworkUrl = api.getCoverArtUrl(coverArt) + artworkUrl = api.getCoverArtUrl(coverArt), + userRating = userRating ) } } \ No newline at end of file diff --git a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/domain/FavoriteSong.kt b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/domain/FavoriteSong.kt index 0419f82b..e9832a3d 100644 --- a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/domain/FavoriteSong.kt +++ b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/domain/FavoriteSong.kt @@ -4,5 +4,6 @@ internal data class FavoriteSong( val id: String, val title: String, val artist: String?, - val artworkUrl: String? + val artworkUrl: String?, + val userRating: Int? ) diff --git a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListScreen.kt b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListScreen.kt index e232d4cd..14556dac 100644 --- a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListScreen.kt +++ b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListScreen.kt @@ -1,10 +1,15 @@ package ru.stresh.youamp.feature.favorite.list.ui import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -18,6 +23,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -28,7 +34,8 @@ import org.koin.androidx.compose.koinViewModel import ru.stersh.youamp.core.ui.Artwork import ru.stersh.youamp.core.ui.EmptyLayout import ru.stersh.youamp.core.ui.ErrorLayout -import ru.stersh.youamp.core.ui.SkeletonScope +import ru.stersh.youamp.core.ui.PlayAllFabButton +import ru.stersh.youamp.core.ui.SkeletonLayout @Composable fun FavoriteListScreen( @@ -41,6 +48,7 @@ fun FavoriteListScreen( FavoriteListScreen( state = state, + onPlayAll = viewModel::playAll, onRetry = viewModel::retry, onRefresh = viewModel::refresh, onSongClick = onSongClick @@ -50,6 +58,7 @@ fun FavoriteListScreen( @Composable private fun FavoriteListScreen( state: FavoriteListStateUi, + onPlayAll: () -> Unit, onRetry: () -> Unit, onRefresh: () -> Unit, onSongClick: (id: String) -> Unit @@ -76,19 +85,32 @@ private fun FavoriteListScreen( } state.favorites?.songs != null -> { - LazyColumn( - modifier = Modifier.fillMaxSize() + Box( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() ) { - items( - items = state.favorites.songs, - contentType = { "song" }, - key = { "song_${it.id}" } - ) { song -> - FavoriteSongItem( - song = song, - onClick = { onSongClick(song.id) } - ) + LazyColumn(modifier = Modifier.fillMaxSize()) { + items( + items = state.favorites.songs, + contentType = { "song" }, + key = { "song_${it.id}" } + ) { song -> + FavoriteSongItem( + song = song, + onClick = { onSongClick(song.id) } + ) + } + item(key = "fab_spacer") { + Spacer(modifier = Modifier.height(88.dp)) + } } + PlayAllFabButton( + onClick = onPlayAll, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) } } } @@ -97,17 +119,17 @@ private fun FavoriteListScreen( @Composable private fun Progress() { - Column { + SkeletonLayout { repeat(10) { ListItem( headlineContent = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - SkeletonScope.SkeletonItem(modifier = Modifier.size(width = 130.dp, height = 16.dp)) - SkeletonScope.SkeletonItem(modifier = Modifier.size(width = 200.dp, height = 16.dp)) + SkeletonItem(modifier = Modifier.size(width = 130.dp, height = 16.dp)) + SkeletonItem(modifier = Modifier.size(width = 200.dp, height = 16.dp)) } }, leadingContent = { - SkeletonScope.SkeletonItem(modifier = Modifier.size(48.dp)) + SkeletonItem(modifier = Modifier.size(48.dp)) } ) } @@ -185,7 +207,7 @@ private fun FavoriteListScreenPreview() { ), ) val state = FavoriteListStateUi( - progress = true, + progress = false, isRefreshing = false, error = false, favorites = FavoritesUi( @@ -194,6 +216,7 @@ private fun FavoriteListScreenPreview() { ) FavoriteListScreen( state = state, + onPlayAll = {}, onRetry = {}, onRefresh = {}, onSongClick = {} diff --git a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListViewModel.kt b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListViewModel.kt index 7e10a02c..cb0a03a3 100644 --- a/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListViewModel.kt +++ b/feature/favorite/list/src/main/java/ru/stresh/youamp/feature/favorite/list/ui/FavoriteListViewModel.kt @@ -6,14 +6,20 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ru.stersh.youamp.shared.player.queue.AudioSource +import ru.stersh.youamp.shared.player.queue.PlayerQueueAudioSourceManager +import ru.stersh.youamp.shared.player.queue.PlayerQueueManager import ru.stresh.youamp.feature.favorite.list.domain.FavoritesRepository import timber.log.Timber internal class FavoriteListViewModel( - private val favoritesRepository: FavoritesRepository + private val favoritesRepository: FavoritesRepository, + private val playerQueueAudioSourceManager: PlayerQueueAudioSourceManager, + private val playerQueueManager: PlayerQueueManager ) : ViewModel() { private val _state = MutableStateFlow(FavoriteListStateUi()) val state: StateFlow @@ -25,6 +31,27 @@ internal class FavoriteListViewModel( retry() } + fun playAll() = viewModelScope.launch { + val favorites = runCatching { favoritesRepository.getFavorites().first() } + .onFailure { Timber.w(it) } + .getOrNull() + ?: return@launch + + favorites.songs.forEach { + playerQueueAudioSourceManager.addSource( + AudioSource.RawSong( + id = it.id, + title = it.title, + artist = it.artist, + artworkUrl = it.artworkUrl, + starred = true, + userRating = it.userRating + ) + ) + } + playerQueueManager.playPosition(0) + } + fun refresh() { _state.update { it.copy(isRefreshing = true) diff --git a/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/AudioSource.kt b/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/AudioSource.kt index 557b17ad..21da22ea 100644 --- a/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/AudioSource.kt +++ b/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/AudioSource.kt @@ -15,4 +15,13 @@ sealed class AudioSource(open val id: String) { override val id: String, val songId: String? = null ) : AudioSource(id) + + data class RawSong( + override val id: String, + val title: String?, + val artist: String?, + val artworkUrl: String?, + val starred: Boolean?, + val userRating: Int? + ) : AudioSource(id) } diff --git a/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/PlayerQueueAudioSourceManagerImpl.kt b/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/PlayerQueueAudioSourceManagerImpl.kt index 554963e9..0ff2ea91 100644 --- a/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/PlayerQueueAudioSourceManagerImpl.kt +++ b/shared/player/src/main/java/ru/stersh/youamp/shared/player/queue/PlayerQueueAudioSourceManagerImpl.kt @@ -14,8 +14,6 @@ import kotlinx.coroutines.coroutineScope import ru.stersh.youamp.core.api.PlaylistEntry import ru.stersh.youamp.core.api.provider.ApiProvider import ru.stersh.youamp.shared.player.android.MusicService -import ru.stersh.youamp.shared.player.utils.MEDIA_ITEM_ALBUM_ID -import ru.stersh.youamp.shared.player.utils.MEDIA_ITEM_DURATION import ru.stersh.youamp.shared.player.utils.MEDIA_SONG_ID import ru.stersh.youamp.shared.player.utils.mediaControllerFuture import ru.stersh.youamp.shared.player.utils.toMediaItem @@ -89,6 +87,7 @@ internal class PlayerQueueAudioSourceManagerImpl( is AudioSource.Album -> getSongs(source) is AudioSource.Artist -> getSongs(source) is AudioSource.Playlist -> getSongs(source) + is AudioSource.RawSong -> listOf(getSong(source)) } } @@ -137,6 +136,48 @@ internal class PlayerQueueAudioSourceManagerImpl( ?: emptyList() } + private suspend fun getSong(source: AudioSource.RawSong): MediaItem { + val songUri = apiProvider + .getApi() + .downloadUrl(source.id) + .toUri() + + val starredRating = HeartRating(source.starred != null) + + val songRating = source.userRating + val rating = if (songRating != null && songRating > 0) { + StarRating(5, songRating.toFloat()) + } else { + StarRating(5) + } + + val metadata = MediaMetadata + .Builder() + .setTitle(source.title) + .setArtist(source.artist) + .setExtras( + bundleOf( + MEDIA_SONG_ID to source.id, + ), + ) + .setUserRating(starredRating) + .setOverallRating(rating) + .setArtworkUri(source.artworkUrl?.toUri()) + .build() + val requestMetadata = MediaItem + .RequestMetadata + .Builder() + .setMediaUri(songUri) + .build() + return MediaItem + .Builder() + .setMediaId(source.id) + .setMediaMetadata(metadata) + .setRequestMetadata(requestMetadata) + .setUri(songUri) + .build() + } + private suspend fun PlaylistEntry.toMediaItem(): MediaItem { val songUri = apiProvider .getApi() @@ -162,8 +203,6 @@ internal class PlayerQueueAudioSourceManagerImpl( .setArtist(artist) .setExtras( bundleOf( - MEDIA_ITEM_ALBUM_ID to albumId, - MEDIA_ITEM_DURATION to (duration ?: 0) * 1000L, MEDIA_SONG_ID to id, ), ) diff --git a/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/Mapper.kt b/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/Mapper.kt index f19e47a6..022c6ff2 100644 --- a/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/Mapper.kt +++ b/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/Mapper.kt @@ -34,8 +34,6 @@ internal suspend fun Song.toMediaItem(apiProvider: ApiProvider): MediaItem { .setArtist(artist) .setExtras( bundleOf( - MEDIA_ITEM_ALBUM_ID to albumId, - MEDIA_ITEM_DURATION to (duration ?: 0) * 1000L, MEDIA_SONG_ID to id, ), ) diff --git a/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/MediaItemExtras.kt b/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/MediaItemExtras.kt index 0c857599..07718cb2 100644 --- a/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/MediaItemExtras.kt +++ b/shared/player/src/main/java/ru/stersh/youamp/shared/player/utils/MediaItemExtras.kt @@ -1,5 +1,3 @@ package ru.stersh.youamp.shared.player.utils -const val MEDIA_ITEM_ALBUM_ID = "album_id" -const val MEDIA_ITEM_DURATION = "duration" const val MEDIA_SONG_ID = "song_id"