From c8d5753046e3b883f0c3353e548270d3c6a029f3 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Thu, 30 Jun 2022 13:49:30 +0200 Subject: [PATCH] Split PlaylistOrAlbumScreen --- .../ui/components/themed/MediaItemMenu.kt | 4 +- .../vimusic/ui/screens/AlbumScreen.kt | 884 ++++++++++++++++++ .../vimusic/ui/screens/ArtistScreen.kt | 4 +- .../vimusic/ui/screens/HomeScreen.kt | 4 +- .../vimusic/ui/screens/IntentUriScreen.kt | 4 +- .../vimusic/ui/screens/LocalPlaylistScreen.kt | 4 +- .../ui/screens/PlaylistOrAlbumScreen.kt | 448 --------- .../vimusic/ui/screens/SearchResultScreen.kt | 4 +- .../vimusic/ui/screens/SearchScreen.kt | 4 +- .../vimusic/ui/screens/SettingsScreen.kt | 4 +- .../it/vfsfitvnm/vimusic/ui/screens/routes.kt | 14 +- .../ui/screens/settings/AboutScreen.kt | 8 +- .../settings/AppearanceSettingsScreen.kt | 4 +- .../settings/BackupAndRestoreScreen.kt | 9 +- .../screens/settings/OtherSettingsScreen.kt | 4 +- .../screens/settings/PlayerSettingsScreen.kt | 4 +- .../it/vfsfitvnm/vimusic/utils/utils.kt | 33 +- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 109 ++- 18 files changed, 1054 insertions(+), 495 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 61281c9..439ebe2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -24,7 +24,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute -import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute +import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers @@ -202,7 +202,7 @@ fun BaseMediaItemMenu( ) { val context = LocalContext.current - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() MediaItemMenu( 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 new file mode 100644 index 0000000..6ee5dde --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt @@ -0,0 +1,884 @@ +package it.vfsfitvnm.vimusic.ui.screens + +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +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.* +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.Playlist +import it.vfsfitvnm.vimusic.models.SongInPlaylist +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.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + + +@ExperimentalAnimationApi +@Composable +fun AlbumScreen( + browseId: String +) { + val lazyListState = rememberLazyListState() + + val albumResult by remember(browseId) { + Database.album(browseId).map { album -> + album?.takeIf { + album.thumbnailUrl != null + }?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId) + .map { youtubeAlbum -> + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url + ).also(Database::update) + } + }.distinctUntilChanged() + }.collectAsState(initial = null, context = Dispatchers.IO) + + val songs by remember(browseId) { + Database.artistSongs(browseId) + }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + + val albumRoute = rememberAlbumRoute() + val artistRoute = rememberArtistRoute() + + RouteHandler(listenToGlobalEmitter = true) { + albumRoute { browseId -> + AlbumScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + artistRoute { browseId -> + ArtistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + host { + val context = LocalContext.current + val density = LocalDensity.current + val binder = LocalPlayerServiceBinder.current + + val colorPalette = LocalColorPalette.current + val typography = LocalTypography.current + val menuState = LocalMenuState.current + + val (thumbnailSizeDp, thumbnailSizePx) = remember { + density.run { + 128.dp to 128.dp.roundToPx() + } + } + + val (songThumbnailSizeDp, songThumbnailSizePx) = remember { + density.run { + 54.dp to 54.dp.roundToPx() + } + } + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 72.dp), + modifier = Modifier + .background(colorPalette.background) + .fillMaxSize() + ) { + item { + TopAppBar( + modifier = Modifier + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = pop) + .padding(vertical = 8.dp) + .padding(horizontal = 16.dp) + .size(24.dp) + ) + + Image( + painter = painterResource(R.drawable.ellipsis_horizontal), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + menuState.display { + Menu { + MenuCloseButton(onClick = menuState::hide) + + MenuEntry( + icon = R.drawable.time, + text = "Enqueue", + onClick = { + menuState.hide() + albumResult + ?.getOrNull() + ?.let { album -> +// album.items +// ?.mapNotNull { song -> +// song.toMediaItem(browseId, album) +// } +// ?.let { mediaItems -> +// binder?.player?.enqueue( +// mediaItems +// ) +// } + } + } + ) + + MenuEntry( + icon = R.drawable.share_social, + text = "Share", + onClick = { + menuState.hide() + + albumResult?.getOrNull()?.shareUrl?.let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + } + ) + } + } + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) + } + } + + item { + albumResult?.getOrNull()?.let { album -> + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Max) + .padding(vertical = 8.dp, horizontal = 16.dp) + .padding(bottom = 16.dp) + ) { + AsyncImage( + model = album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(ThumbnailRoundness.shape) + .size(thumbnailSizeDp) + ) + + Column( + verticalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxSize() + ) { + Column { + BasicText( + text = album.title ?: "Unknown", + style = typography.m.semiBold + ) + + BasicText( + text = buildString { + append(album.authorsText) + if (album.authorsText?.isNotEmpty() == true && album.year != null) { + append(" • ") + } + append(album.year) + }, + style = typography.xs.secondary.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() +// playlistOrAlbum.items +// ?.shuffled() +// ?.mapNotNull { song -> +// song.toMediaItem(browseId, playlistOrAlbum) +// } +// ?.let { mediaItems -> +// binder?.player?.forcePlayFromBeginning( +// mediaItems +// ) +// } + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + + Image( + painter = painterResource(R.drawable.play), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() +// playlistOrAlbum.items +// ?.mapNotNull { song -> +// song.toMediaItem(browseId, playlistOrAlbum) +// } +// ?.let { mediaItems -> +// binder?.player?.forcePlayFromBeginning( +// mediaItems +// ) +// } + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + } + } + } + } ?: albumResult?.exceptionOrNull()?.let { throwable -> + LoadingOrError( + errorMessage = throwable.javaClass.canonicalName, + onRetry = { + query { + runBlocking { + Database.album(browseId).first()?.let(Database::update) + } + } + } + ) + } ?: Loading() + } + +// itemsIndexed( +// items = playlistOrAlbum.valueOrNull?.items ?: emptyList(), +// contentType = { _, song -> song } +// ) { index, song -> +// SongItem( +// title = song.info.name, +// authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, +// durationText = song.durationText, +// onClick = { +// binder?.stopRadio() +// playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> +// song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) +// }?.let { mediaItems -> +// binder?.player?.forcePlayAtIndex(mediaItems, index) +// } +// }, +// startContent = { +// if (song.thumbnail == null) { +// BasicText( +// text = "${index + 1}", +// style = typography.xs.secondary.bold.center, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// modifier = Modifier +// .width(36.dp) +// ) +// } else { +// AsyncImage( +// model = song.thumbnail!!.size(songThumbnailSizePx), +// contentDescription = null, +// contentScale = ContentScale.Crop, +// modifier = Modifier +// .clip(ThumbnailRoundness.shape) +// .size(songThumbnailSizeDp) +// ) +// } +// }, +// menuContent = { +// NonQueuedMediaItemMenu( +// mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) +// ?: return@SongItem, +// onDismiss = menuState::hide, +// ) +// } +// ) +// } + } + } + } +} + + +@ExperimentalAnimationApi +@Composable +fun PlaylistScreen( + browseId: String, +) { + val lazyListState = rememberLazyListState() + + val albumRoute = rememberAlbumRoute() + val artistRoute = rememberArtistRoute() + + RouteHandler(listenToGlobalEmitter = true) { + albumRoute { browseId -> + AlbumScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + artistRoute { browseId -> + ArtistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + host { + val context = LocalContext.current + val density = LocalDensity.current + val binder = LocalPlayerServiceBinder.current + + val colorPalette = LocalColorPalette.current + val typography = LocalTypography.current + val menuState = LocalMenuState.current + + val (thumbnailSizeDp, thumbnailSizePx) = remember { + density.run { + 128.dp to 128.dp.roundToPx() + } + } + + val (songThumbnailSizeDp, songThumbnailSizePx) = remember { + density.run { + 54.dp to 54.dp.roundToPx() + } + } + + var playlistOrAlbum by remember { + mutableStateOf>(Outcome.Loading) + } + + val onLoad = relaunchableEffect(Unit) { + playlistOrAlbum = withContext(Dispatchers.IO) { + YouTube.playlistOrAlbum2(browseId) + } + } + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 72.dp), + modifier = Modifier + .background(colorPalette.background) + .fillMaxSize() + ) { + item { + TopAppBar( + modifier = Modifier + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = pop) + .padding(vertical = 8.dp) + .padding(horizontal = 16.dp) + .size(24.dp) + ) + + Image( + painter = painterResource(R.drawable.ellipsis_horizontal), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + menuState.display { + Menu { + MenuCloseButton(onClick = menuState::hide) + + MenuEntry( + icon = R.drawable.time, + text = "Enqueue", + onClick = { + menuState.hide() + playlistOrAlbum.valueOrNull?.let { album -> + album.items + ?.mapNotNull { song -> + song.toMediaItem(browseId, album) + } + ?.let { mediaItems -> + binder?.player?.enqueue( + mediaItems + ) + } + } + } + ) + + MenuEntry( + icon = R.drawable.list, + text = "Import as playlist", + onClick = { + menuState.hide() + + playlistOrAlbum.valueOrNull?.let { album -> + transaction { + val playlistId = + Database.insert( + Playlist( + name = album.title + ?: "Unknown" + ) + ) + + album.items?.forEachIndexed { index, song -> + song + .toMediaItem(browseId, album) + ?.let { mediaItem -> + Database.insert(mediaItem) + + Database.insert( + SongInPlaylist( + songId = mediaItem.mediaId, + playlistId = playlistId, + position = index + ) + ) + } + } + } + } + } + ) + + MenuEntry( + icon = R.drawable.share_social, + text = "Share", + onClick = { + menuState.hide() + + (playlistOrAlbum.valueOrNull?.url + ?: "https://music.youtube.com/playlist?list=${ + browseId.removePrefix( + "VL" + ) + }").let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + } + ) + } + } + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) + } + } + + item { + OutcomeItem( + outcome = playlistOrAlbum, + onRetry = onLoad, + onLoading = { + Loading() + } + ) { playlistOrAlbum -> + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Max) + .padding(vertical = 8.dp, horizontal = 16.dp) + .padding(bottom = 16.dp) + ) { + AsyncImage( + model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(ThumbnailRoundness.shape) + .size(thumbnailSizeDp) + ) + + Column( + verticalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxSize() + ) { + Column { + BasicText( + text = playlistOrAlbum.title ?: "Unknown", + style = typography.m.semiBold + ) + + BasicText( + text = buildString { + val authors = playlistOrAlbum.authors?.joinToString("") { it.name } + append(authors) + if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) { + append(" • ") + } + append(playlistOrAlbum.year) + }, + style = typography.xs.secondary.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() + playlistOrAlbum.items + ?.shuffled() + ?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum) + } + ?.let { mediaItems -> + binder?.player?.forcePlayFromBeginning( + mediaItems + ) + } + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + + Image( + painter = painterResource(R.drawable.play), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() + playlistOrAlbum.items + ?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum) + } + ?.let { mediaItems -> + binder?.player?.forcePlayFromBeginning( + mediaItems + ) + } + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + } + } + } + } + } + + itemsIndexed( + items = playlistOrAlbum.valueOrNull?.items ?: emptyList(), + contentType = { _, song -> song } + ) { index, song -> + SongItem( + title = song.info.name, + authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, + durationText = song.durationText, + onClick = { + binder?.stopRadio() + playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> + song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) + }?.let { mediaItems -> + binder?.player?.forcePlayAtIndex(mediaItems, index) + } + }, + startContent = { + if (song.thumbnail == null) { + BasicText( + text = "${index + 1}", + style = typography.xs.secondary.bold.center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .width(36.dp) + ) + } else { + AsyncImage( + model = song.thumbnail!!.size(songThumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(ThumbnailRoundness.shape) + .size(songThumbnailSizeDp) + ) + } + }, + menuContent = { + NonQueuedMediaItemMenu( + mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) + ?: return@SongItem, + onDismiss = menuState::hide, + ) + } + ) + } + } + } + } +} + +@Composable +private fun Loading() { + val colorPalette = LocalColorPalette.current + + Column( + modifier = Modifier + .shimmer() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .height(IntrinsicSize.Max) + .padding(vertical = 8.dp, horizontal = 16.dp) + .padding(bottom = 16.dp) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape) + .size(128.dp) + ) + + Column( + verticalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxHeight() + ) { + Column { + TextPlaceholder() + + TextPlaceholder( + modifier = Modifier + .alpha(0.7f) + ) + } + } + } + + repeat(3) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .alpha(0.6f - it * 0.1f) + .height(54.dp) + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + ) { + Spacer( + modifier = Modifier + .size(8.dp) + .background(color = colorPalette.darkGray, shape = CircleShape) + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextPlaceholder() + + TextPlaceholder( + modifier = Modifier + .alpha(0.7f) + ) + } + } + } + } +} + +@Composable +private fun LoadingOrError( + errorMessage: String? = null, + onRetry: (() -> Unit)? = null +) { + val colorPalette = LocalColorPalette.current + + Box { + Column( + modifier = Modifier + .alpha(if (errorMessage == null) 1f else 0f) + .shimmer() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .height(IntrinsicSize.Max) + .padding(vertical = 8.dp, horizontal = 16.dp) + .padding(bottom = 16.dp) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape) + .size(128.dp) + ) + + Column( + verticalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxHeight() + ) { + Column { + TextPlaceholder() + + TextPlaceholder( + modifier = Modifier + .alpha(0.7f) + ) + } + } + } + + repeat(3) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .alpha(0.6f - it * 0.1f) + .height(54.dp) + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + ) { + Spacer( + modifier = Modifier + .size(8.dp) + .background(color = colorPalette.darkGray, shape = CircleShape) + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextPlaceholder() + + 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 966b1d0..81eb040 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 @@ -57,12 +57,12 @@ fun ArtistScreen( ) { val lazyListState = rememberLazyListState() - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index b66484c..69b33c2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -66,7 +66,7 @@ fun HomeScreen() { val playlistRoute = rememberLocalPlaylistRoute() val searchRoute = rememberSearchRoute() val searchResultRoute = rememberSearchResultRoute() - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val playlistPreviews by remember { @@ -128,7 +128,7 @@ fun HomeScreen() { } albumRoute { browseId -> - PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null")) + AlbumScreen(browseId = browseId ?: error("browseId cannot be null")) } artistRoute { browseId -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt index ad50508..8758382 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -45,14 +45,14 @@ import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable fun IntentUriScreen(uri: Uri) { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val lazyListState = rememberLazyListState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt index 9ebd138..88fc130 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt @@ -51,12 +51,12 @@ fun LocalPlaylistScreen( val lazyListState = rememberLazyListState() - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt deleted file mode 100644 index faad681..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistOrAlbumScreen.kt +++ /dev/null @@ -1,448 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.content.Intent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -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 -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.vimusic.models.Playlist -import it.vfsfitvnm.vimusic.models.SongInPlaylist -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 - - -@ExperimentalAnimationApi -@Composable -fun PlaylistOrAlbumScreen( - browseId: String, -) { - val lazyListState = rememberLazyListState() - - var playlistOrAlbum by remember { - mutableStateOf>(Outcome.Loading) - } - - val onLoad = relaunchableEffect(Unit) { - playlistOrAlbum = withContext(Dispatchers.IO) { - YouTube.playlistOrAlbum(browseId) - } - } - - val albumRoute = rememberPlaylistOrAlbumRoute() - val artistRoute = rememberArtistRoute() - - RouteHandler(listenToGlobalEmitter = true) { - albumRoute { browseId -> - PlaylistOrAlbumScreen( - browseId = browseId ?: error("browseId cannot be null") - ) - } - - artistRoute { browseId -> - ArtistScreen( - browseId = browseId ?: error("browseId cannot be null") - ) - } - - host { - val context = LocalContext.current - val density = LocalDensity.current - val binder = LocalPlayerServiceBinder.current - - val colorPalette = LocalColorPalette.current - val typography = LocalTypography.current - val menuState = LocalMenuState.current - - val (thumbnailSizeDp, thumbnailSizePx) = remember { - density.run { - 128.dp to 128.dp.roundToPx() - } - } - - val (songThumbnailSizeDp, songThumbnailSizePx) = remember { - density.run { - 54.dp to 54.dp.roundToPx() - } - } - - val coroutineScope = rememberCoroutineScope() - - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(bottom = 72.dp), - modifier = Modifier - .background(colorPalette.background) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuCloseButton(onClick = menuState::hide) - - MenuEntry( - icon = R.drawable.time, - text = "Enqueue", - onClick = { - menuState.hide() - playlistOrAlbum.valueOrNull?.let { album -> - album.items - ?.mapNotNull { song -> - song.toMediaItem(browseId, album) - } - ?.let { mediaItems -> - binder?.player?.enqueue( - mediaItems - ) - } - } - } - ) - - MenuEntry( - icon = R.drawable.list, - text = "Import as playlist", - onClick = { - menuState.hide() - - playlistOrAlbum.valueOrNull?.let { album -> - transaction { - val playlistId = - Database.insert(Playlist(name = album.title ?: "Unknown")) - - album.items?.forEachIndexed { index, song -> - song - .toMediaItem(browseId, album) - ?.let { mediaItem -> - Database.insert(mediaItem) - - Database.insert( - SongInPlaylist( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) - ) - } - } - } - } - } - ) - - MenuEntry( - icon = R.drawable.share_social, - text = "Share", - onClick = { - menuState.hide() - - (playlistOrAlbum.valueOrNull?.url - ?: "https://music.youtube.com/playlist?list=${browseId.removePrefix("VL")}").let { url -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - - context.startActivity(Intent.createChooser(sendIntent, null)) - } - } - ) - } - } - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - } - - item { - OutcomeItem( - outcome = playlistOrAlbum, - onRetry = onLoad, - onLoading = { - Loading() - } - ) { playlistOrAlbum -> - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 16.dp) - ) { - AsyncImage( - model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(thumbnailSizeDp) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxSize() - ) { - Column { - BasicText( - text = playlistOrAlbum.title ?: "Unknown", - style = typography.m.semiBold - ) - - BasicText( - text = buildString { - val authors = playlistOrAlbum.authors?.joinToString("") { it.name } - append(authors) - if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) { - append(" • ") - } - append(playlistOrAlbum.year) - }, - style = typography.xs.secondary.semiBold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .align(Alignment.End) - .padding(horizontal = 16.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - binder?.stopRadio() - playlistOrAlbum.items - ?.shuffled() - ?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) - }?.let { mediaItems -> - binder?.player?.forcePlayFromBeginning(mediaItems) - } - } - .shadow(elevation = 2.dp, shape = CircleShape) - .background( - color = colorPalette.elevatedBackground, - shape = CircleShape - ) - .padding(horizontal = 16.dp, vertical = 16.dp) - .size(20.dp) - ) - - Image( - painter = painterResource(R.drawable.play), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - binder?.stopRadio() - playlistOrAlbum.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum) - }?.let { mediaItems -> - binder?.player?.forcePlayFromBeginning(mediaItems) - } - } - .shadow(elevation = 2.dp, shape = CircleShape) - .background( - color = colorPalette.elevatedBackground, - shape = CircleShape - ) - .padding(horizontal = 16.dp, vertical = 16.dp) - .size(20.dp) - ) - } - } - } - } - } - - itemsIndexed( - items = playlistOrAlbum.valueOrNull?.items ?: emptyList(), - contentType = { _, song -> song } - ) { index, song -> - SongItem( - title = song.info.name, - authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, - durationText = song.durationText, - onClick = { - binder?.stopRadio() - playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) - }?.let { mediaItems -> - binder?.player?.forcePlayAtIndex(mediaItems, index) - } - }, - startContent = { - if (song.thumbnail == null) { - BasicText( - text = "${index + 1}", - style = typography.xs.secondary.bold.center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .width(36.dp) - ) - } else { - AsyncImage( - model = song.thumbnail!!.size(songThumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(songThumbnailSizeDp) - ) - } - }, - menuContent = { - NonQueuedMediaItemMenu( - mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) - ?: return@SongItem, - onDismiss = menuState::hide, - ) - } - ) - } - } - } - } -} - -@Composable -private fun Loading() { - val colorPalette = LocalColorPalette.current - - Column( - modifier = Modifier - .shimmer() - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 16.dp) - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape) - .size(128.dp) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxHeight() - ) { - Column { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } - } - } - - repeat(3) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .alpha(0.6f - it * 0.1f) - .height(54.dp) - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 16.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(36.dp) - ) { - Spacer( - modifier = Modifier - .size(8.dp) - .background(color = colorPalette.darkGray, shape = CircleShape) - ) - } - - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt index 26507e9..eae6ea2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt @@ -90,14 +90,14 @@ fun SearchResultScreen( } } - val playlistOrAlbumRoute = rememberPlaylistOrAlbumRoute() + val playlistOrAlbumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() RouteHandler( listenToGlobalEmitter = true ) { playlistOrAlbumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: "browseId cannot be null" ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt index 759638b..e75eb9e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt @@ -87,12 +87,12 @@ fun SearchScreen( } }.collectAsState(initial = null, context = Dispatchers.IO) - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt index 7a6618b..1bb063a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt @@ -29,7 +29,7 @@ import it.vfsfitvnm.vimusic.utils.* @ExperimentalAnimationApi @Composable fun SettingsScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val appearanceSettingsRoute = rememberAppearanceSettingsRoute() val playerSettingsRoute = rememberPlayerSettingsRoute() @@ -53,7 +53,7 @@ fun SettingsScreen() { } ) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt index bafc0ab..b705f73 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt @@ -19,12 +19,22 @@ fun rememberIntentUriRoute(): Route1 { } @Composable -fun rememberPlaylistOrAlbumRoute(): Route1 { +fun rememberPlaylistRoute(): Route1 { val browseId = rememberSaveable { mutableStateOf(null) } return remember { - Route1("PlaylistOrAlbumRoute", browseId) + Route1("PlaylistRoute", browseId) + } +} + +@Composable +fun rememberAlbumRoute(): Route1 { + val browseId = rememberSaveable { + mutableStateOf(null) + } + return remember { + Route1("AlbumRoute", browseId) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt index f88fc2c..6c5dd89 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt @@ -17,9 +17,9 @@ import it.vfsfitvnm.vimusic.BuildConfig import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen -import it.vfsfitvnm.vimusic.ui.screens.PlaylistOrAlbumScreen +import it.vfsfitvnm.vimusic.ui.screens.AlbumScreen import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute -import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute +import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.utils.bold @@ -29,14 +29,14 @@ import it.vfsfitvnm.vimusic.utils.semiBold @ExperimentalAnimationApi @Composable fun AboutScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val scrollState = rememberScrollState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt index 40587ce..8b2d397 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt @@ -21,14 +21,14 @@ import it.vfsfitvnm.vimusic.utils.semiBold @ExperimentalAnimationApi @Composable fun AppearanceSettingsScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val scrollState = rememberScrollState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt index 8ceeeb4..384f722 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt @@ -27,12 +27,11 @@ import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen -import it.vfsfitvnm.vimusic.ui.screens.PlaylistOrAlbumScreen +import it.vfsfitvnm.vimusic.ui.screens.AlbumScreen import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute -import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute +import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography -import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import java.io.FileInputStream import java.io.FileOutputStream @@ -44,14 +43,14 @@ import kotlin.system.exitProcess @ExperimentalAnimationApi @Composable fun BackupAndRestoreScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val scrollState = rememberScrollState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt index 57fd3b5..7e87a10 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt @@ -33,14 +33,14 @@ import kotlinx.coroutines.launch @ExperimentalAnimationApi @Composable fun OtherSettingsScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val scrollState = rememberScrollState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt index 0d11f6d..8be6f20 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt @@ -37,14 +37,14 @@ import kotlinx.coroutines.flow.flowOf @ExperimentalAnimationApi @Composable fun PlayerSettingsScreen() { - val albumRoute = rememberPlaylistOrAlbumRoute() + val albumRoute = rememberAlbumRoute() val artistRoute = rememberArtistRoute() val scrollState = rememberScrollState() RouteHandler(listenToGlobalEmitter = true) { albumRoute { browseId -> - PlaylistOrAlbumScreen( + AlbumScreen( browseId = browseId ?: error("browseId cannot be null") ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt index 4eeb68a..5e94a3e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/utils.kt @@ -28,27 +28,35 @@ fun Database.insert(mediaItem: MediaItem): Song { return@runInTransaction it } - val album = mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> - Album( - id = albumId, - title = mediaItem.mediaMetadata.albumTitle!!.toString(), - year = null, - authorsText = null, - thumbnailUrl = null - ).also(::insert) - } - val song = Song( id = mediaItem.mediaId, title = mediaItem.mediaMetadata.title!!.toString(), artistsText = mediaItem.mediaMetadata.artist!!.toString(), - albumId = album?.id, durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!, thumbnailUrl = mediaItem.mediaMetadata.artworkUri!!.toString(), loudnessDb = mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb"), contentLength = mediaItem.mediaMetadata.extras?.getLongOrNull("contentLength"), ).also(::insert) + mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> + Album( + id = albumId, + title = mediaItem.mediaMetadata.albumTitle!!.toString(), + year = null, + authorsText = null, + thumbnailUrl = null, + shareUrl = null, + ).also(::insert) + + insert( + SongAlbumMap( + songId = song.id, + albumId = albumId, + position = null + ) + ) + } + mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames -> mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds -> artistNames.mapIndexed { index, artistName -> @@ -125,12 +133,11 @@ val DetailedSong.asMediaItem: MediaItem MediaMetadata.Builder() .setTitle(song.title) .setArtist(song.artistsText) - .setAlbumTitle(album?.title) .setArtworkUri(song.thumbnailUrl?.toUri()) .setExtras( bundleOf( "videoId" to song.id, - "albumId" to album?.id, + "albumId" to albumId, "artistNames" to artists?.map { it.name }, "artistIds" to artists?.map { it.id }, "durationText" to song.durationText, 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 2ea0ff2..30f275b 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -723,7 +723,114 @@ object YouTube { ) } - suspend fun playlistOrAlbum(browseId: String): Outcome { + 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 -> PlaylistOrAlbum( title = body