diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt index bf3f8da..580da90 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -62,7 +61,7 @@ fun AlbumScreen( album?.takeIf { album.thumbnailUrl != null }?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId) - .map { youtubeAlbum -> + ?.map { youtubeAlbum -> Album( id = browseId, title = youtubeAlbum.title, @@ -337,54 +336,37 @@ private fun LoadingOrError( errorMessage: String? = null, onRetry: (() -> Unit)? = null ) { - val colorPalette = LocalColorPalette.current - - Box { - Column( + LoadingOrError( + errorMessage = errorMessage, + onRetry = onRetry + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier - .alpha(if (errorMessage == null) 1f else 0f) - .shimmer() + .height(IntrinsicSize.Max) + .padding(vertical = 8.dp, horizontal = 16.dp) + .padding(bottom = 16.dp) ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + Spacer( modifier = Modifier - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 16.dp) + .background(color = LocalColorPalette.current.darkGray, shape = ThumbnailRoundness.shape) + .size(128.dp) + ) + + Column( + verticalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxHeight() ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape) - .size(128.dp) - ) + Column { + TextPlaceholder() - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxHeight() - ) { - Column { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } + TextPlaceholder( + modifier = Modifier + .alpha(0.7f) + ) } } } - - errorMessage?.let { - TextCard( - icon = R.drawable.alert_circle, - onClick = onRetry, - modifier = Modifier - .align(Alignment.Center) - ) { - Title(text = onRetry?.let { "Tap to retry" } ?: "Error") - Text(text = "An error has occurred:\n$errorMessage") - } - } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt index 66c5087..37a9897 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import coil.compose.AsyncImage -import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -35,6 +34,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette @@ -85,7 +85,7 @@ fun ArtistScreen( artist?.takeIf { artist.shufflePlaylistId != null }?.let(Result.Companion::success) ?: YouTube.artist(browseId) - .map { youtubeArtist -> + ?.map { youtubeArtist -> Artist( id = browseId, name = youtubeArtist.name, @@ -312,44 +312,29 @@ private fun LoadingOrError( ) { val colorPalette = LocalColorPalette.current - Box { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + LoadingOrError( + errorMessage = errorMessage, + onRetry = onRetry, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( modifier = Modifier - .alpha(if (errorMessage == null) 1f else 0f) - .shimmer() - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.darkGray, shape = CircleShape) - .size(192.dp) - ) + .background(color = colorPalette.darkGray, shape = CircleShape) + .size(192.dp) + ) + TextPlaceholder( + modifier = Modifier + .alpha(0.9f) + .padding(vertical = 8.dp, horizontal = 16.dp) + ) + + repeat(3) { TextPlaceholder( modifier = Modifier - .alpha(0.9f) - .padding(vertical = 8.dp, horizontal = 16.dp) + .alpha(0.8f) + .padding(horizontal = 16.dp) ) - - repeat(3) { - TextPlaceholder( - modifier = Modifier - .alpha(0.8f) - .padding(horizontal = 16.dp) - ) - } - } - - errorMessage?.let { - TextCard( - icon = R.drawable.alert_circle, - onClick = onRetry, - modifier = Modifier - .align(Alignment.Center) - ) { - Title(text = onRetry?.let { "Tap to retry" } ?: "Error") - Text(text = "An error has occurred:\n$errorMessage") - } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt index a0e0da3..71e448c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -35,14 +34,12 @@ import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.* import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.utils.* -import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -92,13 +89,13 @@ fun PlaylistScreen( } } - var playlistOrAlbum by remember { - mutableStateOf>(Outcome.Loading) + var playlist by remember { + mutableStateOf?>(null) } val onLoad = relaunchableEffect(Unit) { - playlistOrAlbum = withContext(Dispatchers.IO) { - YouTube.playlistOrAlbum2(browseId) + playlist = withContext(Dispatchers.IO) { + YouTube.playlistOrAlbum(browseId) } } @@ -140,7 +137,7 @@ fun PlaylistScreen( text = "Enqueue", onClick = { menuState.hide() - playlistOrAlbum.valueOrNull?.let { album -> + playlist?.getOrNull()?.let { album -> album.items ?.mapNotNull { song -> song.toMediaItem(browseId, album) @@ -160,7 +157,7 @@ fun PlaylistScreen( onClick = { menuState.hide() - playlistOrAlbum.valueOrNull?.let { album -> + playlist?.getOrNull()?.let { album -> transaction { val playlistId = Database.insert( @@ -196,7 +193,7 @@ fun PlaylistScreen( onClick = { menuState.hide() - (playlistOrAlbum.valueOrNull?.url + (playlist?.getOrNull()?.url ?: "https://music.youtube.com/playlist?list=${ browseId.removePrefix( "VL" @@ -227,13 +224,7 @@ fun PlaylistScreen( } item { - OutcomeItem( - outcome = playlistOrAlbum, - onRetry = onLoad, - onLoading = { - Loading() - } - ) { playlistOrAlbum -> + playlist?.getOrNull()?.let { playlist -> Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier @@ -243,7 +234,7 @@ fun PlaylistScreen( .padding(bottom = 16.dp) ) { AsyncImage( - model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx), + model = playlist.thumbnail?.size(thumbnailSizePx), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -258,19 +249,19 @@ fun PlaylistScreen( ) { Column { BasicText( - text = playlistOrAlbum.title ?: "Unknown", + text = playlist.title ?: "Unknown", style = typography.m.semiBold ) BasicText( text = buildString { val authors = - playlistOrAlbum.authors?.joinToString("") { it.name } + playlist.authors?.joinToString("") { it.name } append(authors) - if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) { + if (authors?.isNotEmpty() == true && playlist.year != null) { append(" • ") } - append(playlistOrAlbum.year) + append(playlist.year) }, style = typography.xs.secondary.semiBold, maxLines = 2, @@ -291,10 +282,10 @@ fun PlaylistScreen( modifier = Modifier .clickable { binder?.stopRadio() - playlistOrAlbum.items + playlist.items ?.shuffled() ?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) + song.toMediaItem(browseId, playlist) } ?.let { mediaItems -> binder?.player?.forcePlayFromBeginning( @@ -318,9 +309,9 @@ fun PlaylistScreen( modifier = Modifier .clickable { binder?.stopRadio() - playlistOrAlbum.items + playlist.items ?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) + song.toMediaItem(browseId, playlist) } ?.let { mediaItems -> binder?.player?.forcePlayFromBeginning( @@ -339,22 +330,27 @@ fun PlaylistScreen( } } } - } + } ?: playlist?.exceptionOrNull()?.let { throwable -> + LoadingOrError( + errorMessage = throwable.javaClass.canonicalName, + onRetry = onLoad + ) + } ?: LoadingOrError() } itemsIndexed( - items = playlistOrAlbum.valueOrNull?.items ?: emptyList(), + items = playlist?.getOrNull()?.items ?: emptyList(), contentType = { _, song -> song } ) { index, song -> SongItem( title = song.info.name, authors = (song.authors - ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, + ?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name }, durationText = song.durationText, onClick = { binder?.stopRadio() - playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) + playlist?.getOrNull()?.items?.mapNotNull { song -> + song.toMediaItem(browseId, playlist?.getOrNull()!!) }?.let { mediaItems -> binder?.player?.forcePlayAtIndex(mediaItems, index) } @@ -384,7 +380,7 @@ fun PlaylistScreen( NonQueuedMediaItemMenu( mediaItem = song.toMediaItem( browseId, - playlistOrAlbum.valueOrNull!! + playlist?.getOrNull()!! ) ?: return@SongItem, onDismiss = menuState::hide, @@ -397,15 +393,16 @@ fun PlaylistScreen( } } - - @Composable -private fun Loading() { +private fun LoadingOrError( + errorMessage: String? = null, + onRetry: (() -> Unit)? = null +) { val colorPalette = LocalColorPalette.current - Column( - modifier = Modifier - .shimmer() + LoadingOrError( + errorMessage = errorMessage, + onRetry = onRetry ) { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -471,4 +468,4 @@ private fun Loading() { } } } -} \ No newline at end of file +} diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index c9990e0..b1cd3aa 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -653,11 +653,11 @@ object YouTube { class Lyrics( val browseId: String?, ) { - suspend fun text(): Result { + suspend fun text(): Result? { return if (browseId == null) { Result.success(null) } else { - browse2(browseId).map { body -> + browse2(browseId)?.map { body -> body.contents .sectionListRenderer ?.contents @@ -689,8 +689,8 @@ object YouTube { }.bodyCatching() } - suspend fun browse2(browseId: String): Result { - return runCatching { + suspend fun browse2(browseId: String): Result? { + return runCatching { client.post("/youtubei/v1/browse") { contentType(ContentType.Application.Json) setBody( @@ -702,7 +702,7 @@ object YouTube { parameter("key", Key) parameter("prettyPrint", false) }.body() - } + }.recoverIfCancelled() } open class PlaylistOrAlbum( @@ -723,115 +723,8 @@ object YouTube { ) } - suspend fun playlistOrAlbum(browseId: String): Result { - return browse2(browseId).map { body -> - PlaylistOrAlbum( - title = body - .header - ?.musicDetailHeaderRenderer - ?.title - ?.text, - thumbnail = body - .header - ?.musicDetailHeaderRenderer - ?.thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.firstOrNull(), - authors = body - .header - ?.musicDetailHeaderRenderer - ?.subtitle - ?.splitBySeparator() - ?.getOrNull(1) - ?.map { Info.from(it) }, - year = body - .header - ?.musicDetailHeaderRenderer - ?.subtitle - ?.splitBySeparator() - ?.getOrNull(2) - ?.firstOrNull() - ?.text, - items = body - .contents - .singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.contents - ?.firstOrNull() - ?.musicShelfRenderer - ?.contents - ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull { renderer -> - PlaylistOrAlbum.Item( - info = renderer - .flexColumns - .getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.let { Info.from(it) } ?: return@mapNotNull null, - authors = renderer - .flexColumns - .getOrNull(1) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.map { Info.from(it) } - ?.takeIf { it.isNotEmpty() }, - durationText = renderer - .fixedColumns - ?.getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.text, - album = renderer - .flexColumns - .getOrNull(2) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.firstOrNull() - ?.let { Info.from(it) }, - thumbnail = renderer - .thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.firstOrNull() - ) - } - ?.filter { it.info.endpoint != null }, - url = body - .microformat - ?.microformatDataRenderer - ?.urlCanonical, - continuation = body - .contents - .singleColumnBrowseResultsRenderer - ?.tabs - ?.firstOrNull() - ?.tabRenderer - ?.content - ?.sectionListRenderer - ?.continuations - ?.firstOrNull() - ?.nextRadioContinuationData - ?.continuation - ) - } - } - - suspend fun playlistOrAlbum2(browseId: String): Outcome { - return browse(browseId).map { body -> + suspend fun playlistOrAlbum(browseId: String): Result? { + return browse2(browseId)?.map { body -> PlaylistOrAlbum( title = body .header @@ -945,8 +838,8 @@ object YouTube { val radioEndpoint: NavigationEndpoint.Endpoint.Watch? ) - suspend fun artist(browseId: String): Result { - return browse2(browseId).map { body -> + suspend fun artist(browseId: String): Result? { + return browse2(browseId)?.map { body -> Artist( name = body .header