diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt index 4f7e01105..57b5ad4cf 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistItemsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Checkbox @@ -56,6 +57,7 @@ import com.zionhuang.music.ui.component.IconButton import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.YouTubeGridItem import com.zionhuang.music.ui.component.YouTubeListItem +import com.zionhuang.music.ui.component.shimmer.GridItemPlaceHolder import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder import com.zionhuang.music.ui.component.shimmer.ShimmerHost import com.zionhuang.music.ui.menu.YouTubeAlbumMenu @@ -80,6 +82,7 @@ fun ArtistItemsScreen( val mediaMetadata by playerConnection.mediaMetadata.collectAsState() val lazyListState = rememberLazyListState() + val lazyGridState = rememberLazyGridState() val coroutineScope = rememberCoroutineScope() val title by viewModel.title.collectAsState() @@ -117,6 +120,15 @@ fun ArtistItemsScreen( } } + LaunchedEffect(lazyGridState) { + snapshotFlow { + lazyGridState.layoutInfo.visibleItemsInfo.any { it.key == "loading" } + }.collect { shouldLoadMore -> + if (!shouldLoadMore) return@collect + viewModel.loadMore() + } + } + if (itemsPage == null) { ShimmerHost( modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current) @@ -208,6 +220,7 @@ fun ArtistItemsScreen( } } else { LazyVerticalGrid( + state = lazyGridState, columns = GridCells.Adaptive(minSize = GridThumbnailHeight + 24.dp), contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues() ) { @@ -268,6 +281,14 @@ fun ArtistItemsScreen( .animateItem() ) } + + if (itemsPage?.continuation != null) { + item(key = "loading") { + ShimmerHost(Modifier.animateItem()) { + GridItemPlaceHolder(fillMaxWidth = true) + } + } + } } } diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index db91f83aa..62349e442 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -241,7 +241,7 @@ object YouTube { ArtistItemsPage.fromMusicTwoRowItemRenderer(renderer) } }, - continuation = null + continuation = gridRenderer.continuations?.getContinuation() ) } else { ArtistItemsPage( @@ -260,12 +260,24 @@ object YouTube { suspend fun artistItemsContinuation(continuation: String): Result = runCatching { val response = innerTube.browse(WEB_REMIX, continuation = continuation).body() - ArtistItemsContinuationPage( - items = response.continuationContents?.musicPlaylistShelfContinuation?.contents?.mapNotNull { - ArtistItemsContinuationPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer) - }!!, - continuation = response.continuationContents.musicPlaylistShelfContinuation.continuations?.getContinuation() - ) + val gridContinuation = response.continuationContents?.gridContinuation + if (gridContinuation != null) { + ArtistItemsContinuationPage( + items = gridContinuation.items.mapNotNull { + it.musicTwoRowItemRenderer?.let { renderer -> + ArtistItemsPage.fromMusicTwoRowItemRenderer(renderer) + } + }, + continuation = gridContinuation.continuations?.getContinuation() + ) + } else { + ArtistItemsContinuationPage( + items = response.continuationContents?.musicPlaylistShelfContinuation?.contents?.mapNotNull { + ArtistItemsPage.fromMusicResponsiveListItemRenderer(it.musicResponsiveListItemRenderer) + }!!, + continuation = response.continuationContents.musicPlaylistShelfContinuation.continuations?.getContinuation() + ) + } } suspend fun playlist(playlistId: String): Result = runCatching { diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt index 795e8b277..384928c57 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable data class GridRenderer( val header: Header?, val items: List, + val continuations: List?, ) { @Serializable data class Header( diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt index 2273fee40..16fd87cec 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt @@ -2,6 +2,7 @@ package com.zionhuang.innertube.models.response import com.zionhuang.innertube.models.Button import com.zionhuang.innertube.models.Continuation +import com.zionhuang.innertube.models.GridRenderer import com.zionhuang.innertube.models.Menu import com.zionhuang.innertube.models.MusicShelfRenderer import com.zionhuang.innertube.models.ResponseContext @@ -49,6 +50,7 @@ data class BrowseResponse( data class ContinuationContents( val sectionListContinuation: SectionListContinuation?, val musicPlaylistShelfContinuation: MusicPlaylistShelfContinuation?, + val gridContinuation: GridContinuation?, ) { @Serializable data class SectionListContinuation( @@ -61,6 +63,12 @@ data class BrowseResponse( val contents: List, val continuations: List?, ) + + @Serializable + data class GridContinuation( + val items: List, + val continuations: List?, + ) } @Serializable diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsContinuationPage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsContinuationPage.kt index 88b47d799..7b805f772 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsContinuationPage.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/pages/ArtistItemsContinuationPage.kt @@ -1,46 +1,8 @@ package com.zionhuang.innertube.pages -import com.zionhuang.innertube.models.Album -import com.zionhuang.innertube.models.Artist -import com.zionhuang.innertube.models.MusicResponsiveListItemRenderer -import com.zionhuang.innertube.models.SongItem import com.zionhuang.innertube.models.YTItem -import com.zionhuang.innertube.models.oddElements -import com.zionhuang.innertube.utils.parseTime data class ArtistItemsContinuationPage( val items: List, val continuation: String?, -) { - companion object { - fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? { - return SongItem( - id = renderer.playlistItemData?.videoId ?: return null, - title = renderer.flexColumns.firstOrNull() - ?.musicResponsiveListItemFlexColumnRenderer?.text - ?.runs?.firstOrNull()?.text ?: return null, - artists = renderer.flexColumns.getOrNull(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.oddElements()?.map { - Artist( - name = it.text, - id = it.navigationEndpoint?.browseEndpoint?.browseId - ) - } ?: return null, - album = renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let { - Album( - name = it.text, - id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return null - ) - }, - duration = renderer.fixedColumns?.firstOrNull() - ?.musicResponsiveListItemFlexColumnRenderer?.text - ?.runs?.firstOrNull() - ?.text?.parseTime() ?: return null, - thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null, - explicit = renderer.badges?.find { - it.musicInlineBadgeRenderer?.icon?.iconType == "MUSIC_EXPLICIT_BADGE" - } != null, - endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint - ) - } - } -} +)