diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8b869af..4c63468 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -35,7 +35,13 @@
-
+
+
+
- IntentVideoScreen(
- videoId = videoId ?: error("videoId must be not null")
+ intentUriRoute { uri ->
+ IntentUriScreen(
+ uri = uri ?: error("uri must be not null")
)
}
@@ -136,9 +140,7 @@ fun HomeScreen(intentVideoId: String?) {
}
albumRoute { browseId ->
- PlaylistOrAlbumScreen(
- browseId = browseId ?: error("browseId cannot be null")
- )
+ PlaylistOrAlbumScreen(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
new file mode 100644
index 0000000..e9bc748
--- /dev/null
+++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt
@@ -0,0 +1,161 @@
+package it.vfsfitvnm.vimusic.ui.screens
+
+import android.net.Uri
+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.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.valentinilk.shimmer.ShimmerBounds
+import com.valentinilk.shimmer.rememberShimmer
+import it.vfsfitvnm.route.RouteHandler
+import it.vfsfitvnm.vimusic.R
+import it.vfsfitvnm.vimusic.ui.components.Error
+import it.vfsfitvnm.vimusic.ui.components.Message
+import it.vfsfitvnm.vimusic.ui.components.TopAppBar
+import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
+import it.vfsfitvnm.vimusic.utils.*
+import it.vfsfitvnm.youtubemusic.Outcome
+import it.vfsfitvnm.youtubemusic.YouTube
+import it.vfsfitvnm.youtubemusic.toNullable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@ExperimentalAnimationApi
+@Composable
+fun IntentUriScreen(uri: Uri) {
+ val albumRoute = rememberPlaylistOrAlbumRoute()
+ val artistRoute = rememberArtistRoute()
+
+ val lazyListState = rememberLazyListState()
+
+ 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 colorPalette = LocalColorPalette.current
+ val density = LocalDensity.current
+ val player = LocalYoutubePlayer.current
+
+ val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
+
+
+ var items by remember {
+ mutableStateOf>>(Outcome.Loading)
+ }
+
+ val onLoad = relaunchableEffect(Unit) {
+ items = withContext(Dispatchers.IO) {
+ uri.getQueryParameter("list")?.let { playlistId ->
+ YouTube.queue(playlistId).toNullable()?.map { songList ->
+ songList
+ }
+ } ?: uri.getQueryParameter("v")?.let { videoId ->
+ YouTube.song(videoId).toNullable()?.map { listOf(it) }
+ } ?: Outcome.Error.Network
+ }
+ }
+
+ LazyColumn(
+ state = lazyListState,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ contentPadding = PaddingValues(bottom = 64.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)
+ )
+ }
+ }
+
+ when (val currentItems = items) {
+ is Outcome.Error -> item {
+ Error(
+ error = currentItems,
+ onRetry = onLoad,
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ )
+ }
+ is Outcome.Recovered -> item {
+ Error(
+ error = currentItems.error,
+ onRetry = onLoad,
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ )
+ }
+ is Outcome.Loading, is Outcome.Initial -> items(count = 5) { index ->
+ SmallSongItemShimmer(
+ shimmer = shimmer,
+ thumbnailSizeDp = 54.dp,
+ modifier = Modifier
+ .alpha(1f - index * 0.175f)
+ .fillMaxWidth()
+ .padding(vertical = 4.dp, horizontal = 16.dp)
+ )
+ }
+ is Outcome.Success -> {
+ if (currentItems.value.isEmpty()) {
+ item {
+ Message(
+ text = "No songs were found",
+ modifier = Modifier
+ )
+ }
+ } else {
+ itemsIndexed(currentItems.value) { index, item ->
+ SmallSongItem(
+ song = item,
+ thumbnailSizePx = density.run { 54.dp.roundToPx() },
+ onClick = {
+ YoutubePlayer.Radio.reset()
+
+ player?.mediaController?.forcePlayAtIndex(currentItems.value.map(YouTube.Item.Song::asMediaItem), index)
+ pop()
+ }
+ )
+ }
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentVideoScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentVideoScreen.kt
deleted file mode 100644
index 3ec5214..0000000
--- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentVideoScreen.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package it.vfsfitvnm.vimusic.ui.screens
-
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.produceState
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.dp
-import androidx.media3.common.MediaItem
-import com.valentinilk.shimmer.ShimmerBounds
-import com.valentinilk.shimmer.rememberShimmer
-import it.vfsfitvnm.route.RouteHandler
-import it.vfsfitvnm.vimusic.Database
-import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
-import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
-import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
-import it.vfsfitvnm.vimusic.ui.views.SongItem
-import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer
-import it.vfsfitvnm.vimusic.utils.asMediaItem
-import it.vfsfitvnm.vimusic.utils.forcePlay
-import it.vfsfitvnm.youtubemusic.Outcome
-import it.vfsfitvnm.youtubemusic.YouTube
-import it.vfsfitvnm.youtubemusic.toNullable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-@ExperimentalAnimationApi
-@Composable
-fun IntentVideoScreen(videoId: String) {
- 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 colorPalette = LocalColorPalette.current
- val density = LocalDensity.current
- val player = LocalYoutubePlayer.current
-
- val mediaItem by produceState>(initialValue = Outcome.Loading) {
- value = withContext(Dispatchers.IO) {
- Database.songWithInfo(videoId)?.let { songWithInfo ->
- Outcome.Success(songWithInfo.asMediaItem)
- } ?: YouTube.getQueue(videoId).toNullable()
- ?.map(YouTube.Item.Song::asMediaItem)
- ?: Outcome.Error.Network
- }
- }
-
- Column(
- modifier = Modifier
- .background(colorPalette.background)
- .fillMaxSize()
- ) {
- OutcomeItem(
- outcome = mediaItem,
- onLoading = {
- SmallSongItemShimmer(
- shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.View),
- thumbnailSizeDp = 54.dp,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 4.dp, horizontal = 16.dp)
- )
- }
- ) { mediaItem ->
- SongItem(
- mediaItem = mediaItem,
- thumbnailSize = remember {
- density.run {
- 54.dp.roundToPx()
- }
- },
- onClick = {
- player?.mediaController?.forcePlay(mediaItem)
- pop()
- },
- menuContent = {
- NonQueuedMediaItemMenu(mediaItem = mediaItem)
- }
- )
- }
- }
- }
- }
-}
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 94ef451..06c4bd2 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
@@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens
+import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -8,12 +9,12 @@ import it.vfsfitvnm.route.Route0
import it.vfsfitvnm.route.Route1
@Composable
-fun rememberIntentVideoRoute(intentVideoId: String?): Route1 {
- val videoId = rememberSaveable {
- mutableStateOf(intentVideoId)
+fun rememberIntentUriRoute(intentUri: Uri?): Route1 {
+ val uri = rememberSaveable {
+ mutableStateOf(intentUri)
}
return remember {
- Route1("rememberIntentVideoRoute", videoId)
+ Route1("rememberIntentUriRoute", uri)
}
}
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 cf28889..3660974 100644
--- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt
+++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt
@@ -63,7 +63,8 @@ object YouTube {
@Serializable
data class GetQueueBody(
val context: Context,
- val videoIds: List
+ val videoIds: List?,
+ val playlistId: String?,
)
@Serializable
@@ -407,41 +408,65 @@ object YouTube {
}.bodyCatching()
}
- suspend fun getQueue(videoId: String): Outcome {
+ private suspend fun getQueue(body: GetQueueBody): Outcome?> {
return client.postCatching("/youtubei/v1/music/get_queue") {
contentType(ContentType.Application.Json)
- setBody(
- GetQueueBody(
- context = Context.DefaultWeb,
- videoIds = listOf(videoId)
- )
- )
+ setBody(body)
parameter("key", Key)
parameter("prettyPrint", false)
}
.bodyCatching()
.map { body ->
- body.queueDatas?.firstOrNull()?.content?.playlistPanelVideoRenderer?.let { renderer ->
- Item.Song(
- info = Info(
- name = renderer.title.text,
- endpoint = renderer.navigationEndpoint.watchEndpoint
- ),
- authors = renderer.longBylineText?.splitBySeparator()?.getOrNull(0)
- ?.map { run ->
- Info.from(run)
- } ?: emptyList(),
- album = renderer.longBylineText?.splitBySeparator()?.getOrNull(1)?.get(0)
- ?.let { run ->
- Info.from(run)
- },
- thumbnail = renderer.thumbnail.thumbnails[0],
- durationText = renderer.lengthText.text
- )
+ body.queueDatas?.mapNotNull { queueData ->
+ queueData.content?.playlistPanelVideoRenderer?.let { renderer ->
+ Item.Song(
+ info = Info(
+ name = renderer
+ .title
+ .text,
+ endpoint = renderer
+ .navigationEndpoint
+ .watchEndpoint
+ ),
+ authors = renderer
+ .longBylineText
+ ?.splitBySeparator()
+ ?.getOrNull(0)
+ ?.map { Info.from(it) } ?: emptyList(),
+ album = renderer
+ .longBylineText
+ ?.splitBySeparator()
+ ?.getOrNull(1)
+ ?.get(0)
+ ?.let { Info.from(it) },
+ thumbnail = renderer.thumbnail.thumbnails[0],
+ durationText = renderer.lengthText.text
+ )
+ }
}
}
}
+ suspend fun song(videoId: String): Outcome {
+ return getQueue(
+ GetQueueBody(
+ context = Context.DefaultWeb,
+ videoIds = listOf(videoId),
+ playlistId = null
+ )
+ ).map { it?.firstOrNull() }
+ }
+
+ suspend fun queue(playlistId: String): Outcome?> {
+ return getQueue(
+ GetQueueBody(
+ context = Context.DefaultWeb,
+ videoIds = null,
+ playlistId = playlistId
+ )
+ )
+ }
+
suspend fun next(
videoId: String,
playlistId: String?,