From 4a0f60f6f77248cca686c98ed033ab8f15c7dd1f Mon Sep 17 00:00:00 2001 From: aaa1115910 Date: Tue, 22 Oct 2024 19:36:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20PGC=20=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/dev/aaa1115910/bv/BVApp.kt | 16 +- .../aaa1115910/bv/component/pgc/Carousel.kt | 77 ++++ .../aaa1115910/bv/screen/main/PgcContent.kt | 47 ++- .../bv/screen/main/pgc/AnimeContent.kt | 389 ++---------------- .../bv/screen/main/pgc/DocumentaryContent.kt | 21 + .../bv/screen/main/pgc/GuoChuangContent.kt | 21 + .../bv/screen/main/pgc/MovieContent.kt | 21 + .../bv/screen/main/pgc/PgcCommon.kt | 322 +++++++++++++++ .../bv/screen/main/pgc/TvContent.kt | 21 + .../bv/screen/main/pgc/VarietyContent.kt | 21 + .../bv/viewmodel/home/AnimeViewModel.kt | 118 ------ .../bv/viewmodel/pgc/PgcAnimeViewModel.kt | 11 + .../viewmodel/pgc/PgcDocumentaryViewModel.kt | 11 + .../bv/viewmodel/pgc/PgcGuoChuangViewModel.kt | 11 + .../bv/viewmodel/pgc/PgcMovieViewModel.kt | 11 + .../bv/viewmodel/pgc/PgcTvViewModel.kt | 11 + .../bv/viewmodel/pgc/PgcVarietyViewModel.kt | 11 + .../bv/viewmodel/pgc/PgcViewModel.kt | 170 ++++++++ .../biliapi/entity/pgc/PgcCarouselData.kt | 40 ++ .../biliapi/entity/pgc/PgcFeedData.kt | 88 ++++ .../aaa1115910/biliapi/http/BiliHttpApi.kt | 68 +-- .../http/entity/anime/AnimeHomepageData.kt | 43 -- .../http/entity/anime/AnimeHomepageDataV1.kt | 45 -- .../biliapi/http/entity/pgc/PgcFeed.kt | 49 +++ .../{anime/AnimeFeed.kt => pgc/PgcFeedV3.kt} | 9 +- .../PgcWebInitialStateData.kt} | 24 +- .../biliapi/repositories/PgcRepository.kt | 41 ++ .../biliapi/http/BiliHttpApiTest.kt | 31 +- .../biliapi/repositories/PgcRepositoryTest.kt | 33 ++ 29 files changed, 1140 insertions(+), 641 deletions(-) create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/component/pgc/Carousel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/DocumentaryContent.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/GuoChuangContent.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/MovieContent.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/PgcCommon.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/TvContent.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/VarietyContent.kt delete mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt create mode 100644 app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt create mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcCarouselData.kt create mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcFeedData.kt delete mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageData.kt delete mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV1.kt create mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeed.kt rename bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/{anime/AnimeFeed.kt => pgc/PgcFeedV3.kt} (93%) rename bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/{anime/AnimeHomepageDataV2.kt => pgc/PgcWebInitialStateData.kt} (86%) create mode 100644 bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepository.kt create mode 100644 bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepositoryTest.kt diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt b/app/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt index e86dde84..86ac3381 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt @@ -15,6 +15,7 @@ import dev.aaa1115910.biliapi.repositories.FavoriteRepository import dev.aaa1115910.biliapi.repositories.HistoryRepository import dev.aaa1115910.biliapi.repositories.IndexRepository import dev.aaa1115910.biliapi.repositories.LoginRepository +import dev.aaa1115910.biliapi.repositories.PgcRepository import dev.aaa1115910.biliapi.repositories.RecommendVideoRepository import dev.aaa1115910.biliapi.repositories.SearchRepository import dev.aaa1115910.biliapi.repositories.SeasonRepository @@ -34,13 +35,18 @@ import dev.aaa1115910.bv.viewmodel.PlayerViewModel import dev.aaa1115910.bv.viewmodel.TagViewModel import dev.aaa1115910.bv.viewmodel.UserViewModel import dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel -import dev.aaa1115910.bv.viewmodel.home.AnimeViewModel import dev.aaa1115910.bv.viewmodel.home.DynamicViewModel import dev.aaa1115910.bv.viewmodel.home.PopularViewModel import dev.aaa1115910.bv.viewmodel.home.RecommendViewModel import dev.aaa1115910.bv.viewmodel.index.AnimeIndexViewModel import dev.aaa1115910.bv.viewmodel.login.AppQrLoginViewModel import dev.aaa1115910.bv.viewmodel.login.SmsLoginViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcAnimeViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcDocumentaryViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcGuoChuangViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcMovieViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcTvViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcVarietyViewModel import dev.aaa1115910.bv.viewmodel.search.SearchInputViewModel import dev.aaa1115910.bv.viewmodel.search.SearchResultViewModel import dev.aaa1115910.bv.viewmodel.user.FavoriteViewModel @@ -157,6 +163,7 @@ val appModule = module { single { SeasonRepository(get()) } single { dev.aaa1115910.biliapi.repositories.UserRepository(get(), get()) } single { IndexRepository() } + single { PgcRepository() } viewModel { DynamicViewModel(get(), get()) } viewModel { RecommendViewModel(get()) } viewModel { PopularViewModel(get()) } @@ -170,13 +177,18 @@ val appModule = module { viewModel { FollowViewModel(get()) } viewModel { SearchInputViewModel(get()) } viewModel { SearchResultViewModel(get()) } - viewModel { AnimeViewModel() } viewModel { FollowingSeasonViewModel(get()) } viewModel { TagViewModel() } viewModel { VideoPlayerV3ViewModel(get(), get()) } viewModel { VideoDetailViewModel(get()) } viewModel { UserSwitchViewModel(get()) } viewModel { AnimeIndexViewModel(get()) } + viewModel { PgcAnimeViewModel(get()) } + viewModel { PgcGuoChuangViewModel(get()) } + viewModel { PgcDocumentaryViewModel(get()) } + viewModel { PgcMovieViewModel(get()) } + viewModel { PgcTvViewModel(get()) } + viewModel { PgcVarietyViewModel(get()) } } val Context.dataStore: DataStore by preferencesDataStore(name = "Settings") diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/component/pgc/Carousel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/component/pgc/Carousel.kt new file mode 100644 index 00000000..5bfd746e --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/component/pgc/Carousel.kt @@ -0,0 +1,77 @@ +package dev.aaa1115910.bv.component.pgc + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Carousel +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import coil.compose.AsyncImage +import dev.aaa1115910.biliapi.entity.pgc.PgcCarouselData +import dev.aaa1115910.bv.activities.video.SeasonInfoActivity +import dev.aaa1115910.bv.entity.proxy.ProxyArea +import dev.aaa1115910.bv.util.focusedBorder + + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun PgcCarousel( + modifier: Modifier = Modifier, + data: List +) { + val context = LocalContext.current + + Carousel( + itemCount = data.size, + modifier = modifier + //.fillMaxWidth() + .height(240.dp) + .clip(MaterialTheme.shapes.large) + .focusedBorder(), + contentTransformEndToStart = + fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))), + contentTransformStartToEnd = + fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))) + ) { itemIndex -> + PgcCarouselCard( + data = data[itemIndex], + onClick = { + SeasonInfoActivity.actionStart( + context = context, + epId = data[itemIndex].episodeId, + seasonId = data[itemIndex].seasonId, + proxyArea = ProxyArea.checkProxyArea(data[itemIndex].title) + ) + } + ) + } +} + +@Composable +fun PgcCarouselCard( + modifier: Modifier = Modifier, + data: PgcCarouselData.CarouselItem, + onClick: () -> Unit = {} +) { + AsyncImage( + modifier = modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .clickable { onClick() }, + model = data.cover, + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.TopCenter + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt index 9603c2ec..6785897d 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt @@ -24,14 +24,22 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.dp -import dev.aaa1115910.bv.component.DevelopingTipContent -import dev.aaa1115910.bv.component.HomeTopNavItem import dev.aaa1115910.bv.component.PgcTopNavItem import dev.aaa1115910.bv.component.TopNav import dev.aaa1115910.bv.screen.main.pgc.AnimeContent +import dev.aaa1115910.bv.screen.main.pgc.DocumentaryContent +import dev.aaa1115910.bv.screen.main.pgc.GuoChuangContent +import dev.aaa1115910.bv.screen.main.pgc.MovieContent +import dev.aaa1115910.bv.screen.main.pgc.TvContent +import dev.aaa1115910.bv.screen.main.pgc.VarietyContent import dev.aaa1115910.bv.util.fInfo import dev.aaa1115910.bv.util.requestFocus -import dev.aaa1115910.bv.viewmodel.home.AnimeViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcAnimeViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcDocumentaryViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcGuoChuangViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcMovieViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcTvViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcVarietyViewModel import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,7 +49,12 @@ import org.koin.androidx.compose.koinViewModel fun PgcContent( modifier: Modifier = Modifier, navFocusRequester: FocusRequester, - animeViewModel: AnimeViewModel = koinViewModel() + pgcAnimeViewModel: PgcAnimeViewModel = koinViewModel(), + pgcGuoChuangViewModel: PgcGuoChuangViewModel = koinViewModel(), + pgcMovieViewModel: PgcMovieViewModel = koinViewModel(), + pgcDocumentaryViewModel: PgcDocumentaryViewModel = koinViewModel(), + pgcTvViewModel: PgcTvViewModel = koinViewModel(), + pgcVarietyViewModel: PgcVarietyViewModel = koinViewModel() ) { val scope = rememberCoroutineScope() val logger = KotlinLogging.logger("PgcContent") @@ -107,16 +120,12 @@ fun PgcContent( }, onClick = { nav -> when (nav) { - PgcTopNavItem.Anime -> { - logger.fInfo { "reload anime data" } - animeViewModel.reloadAll() - } - - PgcTopNavItem.GuoChuang -> {} - PgcTopNavItem.Movie -> {} - PgcTopNavItem.Documentary -> {} - PgcTopNavItem.Tv -> {} - PgcTopNavItem.Variety -> {} + PgcTopNavItem.Anime -> pgcAnimeViewModel.reloadAll() + PgcTopNavItem.GuoChuang -> pgcGuoChuangViewModel.reloadAll() + PgcTopNavItem.Movie -> pgcMovieViewModel.reloadAll() + PgcTopNavItem.Documentary -> pgcDocumentaryViewModel.reloadAll() + PgcTopNavItem.Tv -> pgcTvViewModel.reloadAll() + PgcTopNavItem.Variety -> pgcVarietyViewModel.reloadAll() } } ) @@ -143,11 +152,11 @@ fun PgcContent( ) { screen -> when (screen) { PgcTopNavItem.Anime -> AnimeContent(lazyListState = animeState) - PgcTopNavItem.GuoChuang -> DevelopingTipContent() - PgcTopNavItem.Movie -> DevelopingTipContent() - PgcTopNavItem.Documentary -> DevelopingTipContent() - PgcTopNavItem.Tv -> DevelopingTipContent() - PgcTopNavItem.Variety -> DevelopingTipContent() + PgcTopNavItem.GuoChuang -> GuoChuangContent(lazyListState = guoChuangState) + PgcTopNavItem.Movie -> MovieContent(lazyListState = movieState) + PgcTopNavItem.Documentary -> DocumentaryContent(lazyListState = documentaryState) + PgcTopNavItem.Tv -> TvContent(lazyListState = tvState) + PgcTopNavItem.Variety -> VarietyContent(lazyListState = varietyState) } } } diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt index 0d7fa646..148b535e 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt @@ -1,221 +1,88 @@ package dev.aaa1115910.bv.screen.main.pgc import android.content.Intent -import android.view.KeyEvent -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.List import androidx.compose.material.icons.rounded.Alarm import androidx.compose.material.icons.rounded.Favorite import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.tv.material3.Carousel import androidx.tv.material3.ClickableSurfaceDefaults -import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Surface import androidx.tv.material3.Text -import coil.compose.AsyncImage -import dev.aaa1115910.biliapi.http.entity.anime.AnimeFeedData -import dev.aaa1115910.biliapi.http.entity.anime.CarouselItem -import dev.aaa1115910.biliapi.http.entity.web.Hover +import dev.aaa1115910.biliapi.repositories.PgcType import dev.aaa1115910.bv.R import dev.aaa1115910.bv.activities.anime.AnimeIndexActivity import dev.aaa1115910.bv.activities.anime.AnimeTimelineActivity import dev.aaa1115910.bv.activities.user.FollowingSeasonActivity -import dev.aaa1115910.bv.activities.video.SeasonInfoActivity -import dev.aaa1115910.bv.component.videocard.SeasonCard -import dev.aaa1115910.bv.entity.carddata.SeasonCardData -import dev.aaa1115910.bv.entity.proxy.ProxyArea import dev.aaa1115910.bv.ui.theme.BVTheme -import dev.aaa1115910.bv.util.ImageSize -import dev.aaa1115910.bv.util.focusedBorder -import dev.aaa1115910.bv.util.resizedImageUrl import dev.aaa1115910.bv.util.toast -import dev.aaa1115910.bv.viewmodel.home.AnimeViewModel +import dev.aaa1115910.bv.viewmodel.pgc.PgcAnimeViewModel import org.koin.androidx.compose.koinViewModel @Composable fun AnimeContent( modifier: Modifier = Modifier, lazyListState: LazyListState, - animeViewModel: AnimeViewModel = koinViewModel() + pgcViewModel: PgcAnimeViewModel = koinViewModel() ) { val context = LocalContext.current - val carouselFocusRequester = remember { FocusRequester() } - val carouselItems = animeViewModel.carouselItems - val animeFeeds = animeViewModel.feedItems - - LazyColumn( - modifier = modifier, - state = lazyListState - ) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.Center - ) { - AnimeCarousel( - modifier = Modifier - .width(880.dp) - .padding(32.dp, 0.dp) - .focusRequester(carouselFocusRequester), - data = carouselItems - ) - } + val onOpenTimeline: () -> Unit = { + context.startActivity(Intent(context, AnimeTimelineActivity::class.java)) + } + val onOpenFollowing: () -> Unit = { + context.startActivity(Intent(context, FollowingSeasonActivity::class.java)) + } + val onOpenIndex: () -> Unit = { + context.startActivity(Intent(context, AnimeIndexActivity::class.java)) + } + val onOpenGamerAni: () -> Unit = { + val packageManager = context.packageManager + val gamerAniPackageName = "tw.com.gamer.android.animad" + packageManager.getLeanbackLaunchIntentForPackage(gamerAniPackageName)?.let { + context.startActivity(it) + } ?: run { + R.string.anime_home_button_gamer_ani_launch_failed.toast(context) } - item { + } + + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.Anime, + featureButtons = { AnimeFeatureButtons( modifier = Modifier.padding(vertical = 24.dp), - onOpenTimeline = { - context.startActivity(Intent(context, AnimeTimelineActivity::class.java)) - }, - onOpenFollowing = { - context.startActivity(Intent(context, FollowingSeasonActivity::class.java)) - }, - onOpenIndex = { - context.startActivity(Intent(context, AnimeIndexActivity::class.java)) - }, - onOpenGamerAni = { - val packageManager = context.packageManager - val gamerAniPackageName = "tw.com.gamer.android.animad" - packageManager.getLeanbackLaunchIntentForPackage(gamerAniPackageName)?.let { - context.startActivity(it) - } ?: let { - R.string.anime_home_button_gamer_ani_launch_failed.toast(context) - } - } + onOpenTimeline = onOpenTimeline, + onOpenFollowing = onOpenFollowing, + onOpenIndex = onOpenIndex, + onOpenGamerAni = onOpenGamerAni ) } - itemsIndexed(items = animeFeeds) { index, feedItems -> - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp) - .onFocusChanged { - if (it.hasFocus) { - if (index + 10 > animeFeeds.size) { - animeViewModel.loadMore() - } - } - }, - contentAlignment = Alignment.Center - ) { - when (feedItems.firstOrNull()?.cardStyle) { - "v_card" -> AnimeFeedVideoRow( - data = feedItems - ) - - "rank" -> AnimeFeedRankRow( - data = feedItems - ) - } - } - } - } -} - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -fun AnimeCarousel( - modifier: Modifier = Modifier, - data: List -) { - val context = LocalContext.current - - Carousel( - itemCount = data.size, - modifier = modifier - //.fillMaxWidth() - .height(240.dp) - .clip(MaterialTheme.shapes.large) - .focusedBorder(), - contentTransformEndToStart = - fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))), - contentTransformStartToEnd = - fadeIn(tween(1000)).togetherWith(fadeOut(tween(1000))) - ) { itemIndex -> - AnimeCarouselCard( - data = data[itemIndex], - onClick = { - SeasonInfoActivity.actionStart( - context = context, - epId = data[itemIndex].episodeId, - seasonId = data[itemIndex].seasonId, - proxyArea = ProxyArea.checkProxyArea(data[itemIndex].title) - ) - } - ) - } -} - -@Composable -fun AnimeCarouselCard( - modifier: Modifier = Modifier, - data: CarouselItem, - onClick: () -> Unit = {} -) { - AsyncImage( - modifier = modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .clickable { onClick() }, - model = data.cover, - contentDescription = null, - contentScale = ContentScale.FillWidth, - alignment = Alignment.TopCenter ) } @@ -354,170 +221,6 @@ fun AnimeFeatureButton( } } -@Composable -fun AnimeFeedVideoRow( - modifier: Modifier = Modifier, - data: List -) { - val context = LocalContext.current - LazyRow( - modifier = modifier, - contentPadding = PaddingValues(horizontal = 24.dp), - horizontalArrangement = Arrangement.spacedBy(24.dp) - ) { - data.forEachIndexed { index, feedItem -> - val cardModifier = if (index == data.lastIndex) { - Modifier.onPreviewKeyEvent { - when (it.key) { - Key.DirectionRight -> return@onPreviewKeyEvent true - } - false - } - } else { - Modifier - } - - item { - SeasonCard( - modifier = cardModifier, - coverHeight = 180.dp, - data = SeasonCardData( - seasonId = feedItem.seasonId ?: 0, - title = feedItem.title, - subTitle = feedItem.subTitle, - cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail), - rating = feedItem.rating ?: "" - ), - onClick = { - SeasonInfoActivity.actionStart( - context = context, - seasonId = feedItem.seasonId, - proxyArea = ProxyArea.checkProxyArea(feedItem.title) - ) - } - ) - } - } - } -} - -@Composable -fun AnimeFeedRankRow( - modifier: Modifier = Modifier, - data: List -) { - val context = LocalContext.current - Box( - modifier = modifier - .height(300.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.verticalGradient( - colors = listOf( - // light theme color: Color(250, 222, 214) - Color(20, 18, 17), - Color(20, 18, 17).copy(alpha = 0.298f) - ) - ) - ) - ) {} - BoxWithConstraints { - AsyncImage( - modifier = Modifier - .fillMaxHeight() - .offset(x = (-1 * (0.25 * 1.6 * this.maxHeight.value)).dp) - .graphicsLayer { alpha = 0.99f } - .drawWithContent { - val colors = listOf( - Color.Black, - Color.Transparent - ) - drawContent() - drawRect( - brush = Brush.horizontalGradient(colors), - blendMode = BlendMode.DstIn - ) - drawRect( - brush = Brush.verticalGradient(colors), - blendMode = BlendMode.DstIn - ) - }, - model = data.first().cover, - contentDescription = null, - contentScale = ContentScale.FillHeight, - alpha = 1f - ) - } - Row( - modifier = Modifier - .fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier - .fillMaxHeight() - .width(240.dp) - .padding(32.dp), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.End - ) { - Text( - text = data.first().title, - style = MaterialTheme.typography.titleLarge, - color = Color.White - ) - Text( - text = data.first().subTitle, - style = MaterialTheme.typography.bodySmall, - color = Color.White.copy(alpha = 0.6f) - ) - } - - LazyRow( - modifier = modifier, - contentPadding = PaddingValues(horizontal = 32.dp), - horizontalArrangement = Arrangement.spacedBy(18.dp) - ) { - data.first().subItems?.forEachIndexed { index, feedItem -> - val cardModifier = if (index == data.first().subItems?.lastIndex) { - Modifier.onPreviewKeyEvent { - when (it.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_RIGHT -> return@onPreviewKeyEvent true - } - false - } - } else { - Modifier - } - - item { - SeasonCard( - modifier = cardModifier, - coverHeight = 180.dp, - data = SeasonCardData( - seasonId = feedItem.seasonId ?: 0, - title = feedItem.title, - subTitle = feedItem.subTitle, - cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail), - rating = feedItem.rating ?: "" - ), - onClick = { - SeasonInfoActivity.actionStart( - context = context, - seasonId = feedItem.seasonId, - proxyArea = ProxyArea.checkProxyArea(feedItem.title) - ) - } - ) - } - } - } - } - } -} @Preview(device = "id:tv_1080p") @@ -533,35 +236,3 @@ fun AnimeFeatureButtonsPreview() { ) } } - -@Preview(device = "id:tv_1080p") -@Composable -fun AnimeFeedRankRowPreview() { - val data = listOf( - AnimeFeedData.FeedItem.FeedSubItem( - cardStyle = "rank", - rankId = 126, - cover = "http://i0.hdslb.com/bfs/archive/aae451dabf64ead2e983f92be76039a8ba233ade.png", - title = "热门热血番剧榜", - subTitle = "每小时更新", - report = AnimeFeedData.FeedItem.FeedSubItem.Report(), - subItems = List(8) { - AnimeFeedData.FeedItem.FeedSubItem( - cardStyle = "v_card", - rankId = 0, - cover = "https://i0.hdslb.com/bfs/bangumi/image/f610305ad3922bee9d51748ab38da0c54e785b44.png", - hover = Hover( - img = "http://i0.hdslb.com/bfs/archive/aae451dabf64ead2e983f92be76039a8ba233ade.png", - text = listOf("漫画改", "热血", "更新至第6话") - ), - title = "解雇后走上人生巅峰", - subTitle = "被解雇的暗黑士兵慢生活的第二人生", - report = AnimeFeedData.FeedItem.FeedSubItem.Report() - ) - } - ) - ) - BVTheme { - AnimeFeedRankRow(data = data) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/DocumentaryContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/DocumentaryContent.kt new file mode 100644 index 00000000..0d55365b --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/DocumentaryContent.kt @@ -0,0 +1,21 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.viewmodel.pgc.PgcDocumentaryViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun DocumentaryContent( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcDocumentaryViewModel = koinViewModel() +) { + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.Documentary + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/GuoChuangContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/GuoChuangContent.kt new file mode 100644 index 00000000..ef1f259b --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/GuoChuangContent.kt @@ -0,0 +1,21 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.viewmodel.pgc.PgcGuoChuangViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun GuoChuangContent( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcGuoChuangViewModel = koinViewModel() +) { + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.GuoChuang + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/MovieContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/MovieContent.kt new file mode 100644 index 00000000..9c8a4913 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/MovieContent.kt @@ -0,0 +1,21 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.viewmodel.pgc.PgcMovieViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun MovieContent( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcMovieViewModel = koinViewModel() +) { + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.Movie + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/PgcCommon.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/PgcCommon.kt new file mode 100644 index 00000000..96c075e8 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/PgcCommon.kt @@ -0,0 +1,322 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import android.view.KeyEvent +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import coil.compose.AsyncImage +import dev.aaa1115910.biliapi.entity.pgc.PgcFeedData +import dev.aaa1115910.biliapi.http.SeasonIndexType +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.activities.video.SeasonInfoActivity +import dev.aaa1115910.bv.component.pgc.PgcCarousel +import dev.aaa1115910.bv.component.videocard.SeasonCard +import dev.aaa1115910.bv.entity.carddata.SeasonCardData +import dev.aaa1115910.bv.entity.proxy.ProxyArea +import dev.aaa1115910.bv.ui.theme.BVTheme +import dev.aaa1115910.bv.util.ImageSize +import dev.aaa1115910.bv.util.resizedImageUrl +import dev.aaa1115910.bv.viewmodel.pgc.FeedListType +import dev.aaa1115910.bv.viewmodel.pgc.PgcViewModel + +@Composable +fun PgcScaffold( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcViewModel, + pgcType: PgcType, + featureButtons: (@Composable () -> Unit)? = null +) { + val carouselFocusRequester = remember { FocusRequester() } + + val carouselItems = pgcViewModel.carouselItems + val pgcFeeds = pgcViewModel.feedItems + + LazyColumn( + modifier = modifier, + state = lazyListState + ) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.Center + ) { + PgcCarousel( + modifier = Modifier + .width(880.dp) + .padding(32.dp, 0.dp) + .focusRequester(carouselFocusRequester), + data = carouselItems + ) + } + } + if (featureButtons != null) { + item { + featureButtons() + } + }else{ + item { + Spacer( + modifier=Modifier + .fillMaxWidth() + .height(24.dp) + ) + } + } + itemsIndexed(items = pgcFeeds) { index, feedListItem -> + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + .onFocusChanged { + if (it.hasFocus) { + if (index + 10 > pgcFeeds.size) { + pgcViewModel.loadMore() + } + } + }, + contentAlignment = Alignment.Center + ) { + when (feedListItem.type) { + FeedListType.Ep -> PgcFeedVideoRow( + data = feedListItem.items!! + ) + + FeedListType.Rank -> PgcFeedRankRow( + data = feedListItem.rank!! + ) + } + } + } + } +} + +@Composable +fun PgcFeedVideoRow( + modifier: Modifier = Modifier, + data: List +) { + val context = LocalContext.current + LazyRow( + modifier = modifier, + contentPadding = PaddingValues(horizontal = 24.dp), + horizontalArrangement = Arrangement.spacedBy(24.dp) + ) { + data.forEachIndexed { index, feedItem -> + val cardModifier = if (index == data.lastIndex) { + Modifier.onPreviewKeyEvent { + when (it.key) { + Key.DirectionRight -> return@onPreviewKeyEvent true + } + false + } + } else { + Modifier + } + + item { + SeasonCard( + modifier = cardModifier, + coverHeight = 180.dp, + data = SeasonCardData( + seasonId = feedItem.seasonId, + title = feedItem.title, + subTitle = feedItem.subTitle, + cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail), + rating = feedItem.rating + ), + onClick = { + SeasonInfoActivity.actionStart( + context = context, + seasonId = feedItem.seasonId, + proxyArea = ProxyArea.checkProxyArea(feedItem.title) + ) + } + ) + } + } + } +} + +@Composable +fun PgcFeedRankRow( + modifier: Modifier = Modifier, + data: PgcFeedData.FeedRank +) { + val context = LocalContext.current + Box( + modifier = modifier + .height(300.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + // light theme color: Color(250, 222, 214) + Color(20, 18, 17), + Color(20, 18, 17).copy(alpha = 0.298f) + ) + ) + ) + ) {} + BoxWithConstraints { + AsyncImage( + modifier = Modifier + .fillMaxHeight() + .offset(x = (-1 * (0.25 * 1.6 * this.maxHeight.value)).dp) + .graphicsLayer { alpha = 0.99f } + .drawWithContent { + val colors = listOf( + Color.Black, + Color.Transparent + ) + drawContent() + drawRect( + brush = Brush.horizontalGradient(colors), + blendMode = BlendMode.DstIn + ) + drawRect( + brush = Brush.verticalGradient(colors), + blendMode = BlendMode.DstIn + ) + }, + model = data.cover, + contentDescription = null, + contentScale = ContentScale.FillHeight, + alpha = 1f + ) + } + Row( + modifier = Modifier + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .width(240.dp) + .padding(32.dp), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.End + ) { + Text( + text = data.title, + style = MaterialTheme.typography.titleLarge, + color = Color.White + ) + Text( + text = data.subTitle, + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.6f) + ) + } + + LazyRow( + modifier = modifier, + contentPadding = PaddingValues(horizontal = 32.dp), + horizontalArrangement = Arrangement.spacedBy(18.dp) + ) { + data.items.forEachIndexed { index, feedItem -> + val cardModifier = if (index == data.items.lastIndex) { + Modifier.onPreviewKeyEvent { + when (it.nativeKeyEvent.keyCode) { + KeyEvent.KEYCODE_DPAD_RIGHT -> return@onPreviewKeyEvent true + } + false + } + } else { + Modifier + } + + item { + SeasonCard( + modifier = cardModifier, + coverHeight = 180.dp, + data = SeasonCardData( + seasonId = feedItem.seasonId, + title = feedItem.title, + subTitle = feedItem.subTitle, + cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail), + rating = feedItem.rating + ), + onClick = { + SeasonInfoActivity.actionStart( + context = context, + seasonId = feedItem.seasonId, + proxyArea = ProxyArea.checkProxyArea(feedItem.title) + ) + } + ) + } + } + } + } + } +} + +@Preview(device = "id:tv_1080p") +@Composable +fun PgcFeedRankRowPreview() { + val data = PgcFeedData.FeedRank( + cover = "http://i0.hdslb.com/bfs/archive/aae451dabf64ead2e983f92be76039a8ba233ade.png", + title = "热门热血番剧榜", + subTitle = "每小时更新", + items = List(8) { + PgcFeedData.FeedItem( + cover = "https://i0.hdslb.com/bfs/bangumi/image/f610305ad3922bee9d51748ab38da0c54e785b44.png", + title = "解雇后走上人生巅峰", + subTitle = "被解雇的暗黑士兵慢生活的第二人生", + episodeId = 0, + seasonId = 0, + seasonType = SeasonIndexType.Anime, + rating = "9.8" + ) + } + ) + BVTheme { + PgcFeedRankRow(data = data) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/TvContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/TvContent.kt new file mode 100644 index 00000000..085a48dd --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/TvContent.kt @@ -0,0 +1,21 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.viewmodel.pgc.PgcTvViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun TvContent( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcTvViewModel = koinViewModel() +) { + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.Tv + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/VarietyContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/VarietyContent.kt new file mode 100644 index 00000000..20dfa4de --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/VarietyContent.kt @@ -0,0 +1,21 @@ +package dev.aaa1115910.bv.screen.main.pgc + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.viewmodel.pgc.PgcVarietyViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun VarietyContent( + modifier: Modifier = Modifier, + lazyListState: LazyListState, + pgcViewModel: PgcVarietyViewModel = koinViewModel() +) { + PgcScaffold( + lazyListState = lazyListState, + pgcViewModel = pgcViewModel, + pgcType = PgcType.Variety + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt deleted file mode 100644 index e347a791..00000000 --- a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt +++ /dev/null @@ -1,118 +0,0 @@ -package dev.aaa1115910.bv.viewmodel.home - -import androidx.compose.runtime.mutableStateListOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dev.aaa1115910.biliapi.http.BiliHttpApi -import dev.aaa1115910.biliapi.http.entity.anime.AnimeFeedData -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageDataType -import dev.aaa1115910.biliapi.http.entity.anime.CarouselItem -import dev.aaa1115910.bv.BVApp -import dev.aaa1115910.bv.util.fInfo -import dev.aaa1115910.bv.util.toast -import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class AnimeViewModel : ViewModel() { - companion object { - private val logger = KotlinLogging.logger { } - } - - val carouselItems = mutableStateListOf() - val feedItems = mutableStateListOf>() - private val restSubItems = mutableListOf() - - private var updating = false - private var cursor = 0 - private var hasNext = true - - init { - loadMore() - viewModelScope.launch(Dispatchers.Default) { - updateCarousel() - } - } - - fun loadMore() { - if (hasNext) { - viewModelScope.launch(Dispatchers.Default) { - updateFeed() - } - } - } - - private fun clearAll() { - logger.fInfo { "Clear all data" } - carouselItems.clear() - feedItems.clear() - restSubItems.clear() - cursor = 0 - hasNext = true - } - - fun reloadAll() { - logger.fInfo { "Reload all" } - clearAll() - viewModelScope.launch(Dispatchers.IO) { - updateCarousel() - updateFeed() - } - } - - private suspend fun updateCarousel() { - logger.fInfo { "Update anime carousel" } - runCatching { - val items = BiliHttpApi.getAnimeHomepageData(dataType = AnimeHomepageDataType.V2) - ?.getCarouselItems() ?: emptyList() - logger.fInfo { "Find anime carousels, size: ${items.size}" } - carouselItems.addAll(items) - }.onFailure { - logger.fInfo { "Update anime carousel failed: ${it.stackTraceToString()}" } - withContext(Dispatchers.Main) { - "加载轮播图失败: ${it.message}".toast(BVApp.context) - } - } - } - - private suspend fun updateFeed() { - if (updating) return - updating = true - logger.fInfo { "Update anime feed" } - runCatching { - val responseData = BiliHttpApi.getAnimeFeed(cursor = cursor).getResponseData() - cursor = responseData.coursor - hasNext = responseData.hasNext - updateFeedItems(responseData.items) - }.onFailure { - logger.fInfo { "Update anime feeds failed: ${it.stackTraceToString()}" } - } - updating = false - } - - private fun updateFeedItems(items: List) { - val vCardList = mutableListOf() - val rankList = mutableStateListOf() - - vCardList.addAll(restSubItems) - items.forEach { feedItem -> - when (feedItem.subItems.firstOrNull()?.cardStyle) { - "v_card" -> vCardList.addAll(feedItem.subItems) - "rank" -> rankList.add(feedItem) - } - } - - vCardList.chunked(5).forEach { chunkedVCardList -> - if (chunkedVCardList.size == 5) { - feedItems.add(chunkedVCardList) - } else { - restSubItems.clear() - restSubItems.addAll(chunkedVCardList) - } - } - rankList.forEach { rankListItem -> - rankListItem.subItems.forEach { feedItems.add(listOf(it)) } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt new file mode 100644 index 00000000..842b68df --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcAnimeViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.Anime +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt new file mode 100644 index 00000000..3bc460d3 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcDocumentaryViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.Documentary +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt new file mode 100644 index 00000000..58f02dde --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcGuoChuangViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.GuoChuang +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt new file mode 100644 index 00000000..1f38f857 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcMovieViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.Movie +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt new file mode 100644 index 00000000..0ed5e0d9 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcTvViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.Tv +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt new file mode 100644 index 00000000..55cdaa4f --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt @@ -0,0 +1,11 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType + +class PgcVarietyViewModel( + override val pgcRepository: PgcRepository +) : PgcViewModel( + pgcRepository = pgcRepository, + pgcType = PgcType.Variety +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt new file mode 100644 index 00000000..acd3e2ca --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt @@ -0,0 +1,170 @@ +package dev.aaa1115910.bv.viewmodel.pgc + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.aaa1115910.biliapi.entity.pgc.PgcCarouselData +import dev.aaa1115910.biliapi.entity.pgc.PgcFeedData +import dev.aaa1115910.biliapi.repositories.PgcRepository +import dev.aaa1115910.biliapi.repositories.PgcType +import dev.aaa1115910.bv.BVApp +import dev.aaa1115910.bv.util.fInfo +import dev.aaa1115910.bv.util.toast +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +abstract class PgcViewModel( + open val pgcRepository: PgcRepository, + val pgcType: PgcType, +) : ViewModel() { + private val logger = KotlinLogging.logger("PgcViewModel[$pgcType]") + + /** + * 轮播图 + */ + val carouselItems = mutableStateListOf() + + /** + * 猜你喜欢 + */ + val feedItems = mutableStateListOf() + + /** + * 推荐数据中会穿插排行榜,为了避免出现某一行仅出现单独几个剧集,因此将不满一行的剧集单独存起来 + */ + private val restSubItems = mutableListOf() + + var updating by mutableStateOf(false) + var hasNext by mutableStateOf(true) + var cursor by mutableIntStateOf(0) + + init { + loadMore() + viewModelScope.launch(Dispatchers.IO) { + updateCarousel() + } + } + + /** + * 加载更多推荐数据 + */ + fun loadMore() { + if (hasNext) { + viewModelScope.launch(Dispatchers.IO) { + updateFeed() + } + } + } + + /** + * 重新加载所有数据,点击界面顶部 Tab 时使用 + */ + fun reloadAll() { + logger.fInfo { "Reload all $pgcType data" } + clearAll() + viewModelScope.launch(Dispatchers.IO) { + updateCarousel() + updateFeed() + } + } + + /** + * 清理所有数据 + */ + fun clearAll() { + logger.fInfo { "Clear all data" } + carouselItems.clear() + feedItems.clear() + restSubItems.clear() + cursor = 0 + hasNext = true + } + + /** + * 更新轮播图 + */ + private suspend fun updateCarousel() { + logger.fInfo { "Updating $pgcType carousel" } + runCatching { + val carouselData = pgcRepository.getCarousel(pgcType) + logger.fInfo { "Find $pgcType carousels, size: ${carouselData.items.size}" } + carouselItems.addAll(carouselData.items) + logger.debug { "carouselItems: $carouselItems" } + }.onFailure { + logger.fInfo { "Update $pgcType carousel failed: ${it.stackTraceToString()}" } + withContext(Dispatchers.Main) { + "加载 $pgcType 轮播图失败: ${it.message}".toast(BVApp.context) + } + } + } + + /** + * 获取推荐数据 + */ + private suspend fun updateFeed() { + if (updating) return + updating = true + logger.fInfo { "Update anime feed" } + runCatching { + val pgcFeedData = pgcRepository.getFeed( + pgcType = pgcType, + cursor = cursor + ) + cursor = pgcFeedData.cursor + hasNext = pgcFeedData.hasNext + updateFeedItems(pgcFeedData) + }.onFailure { + logger.fInfo { "Update $pgcType feeds failed: ${it.stackTraceToString()}" } + } + updating = false + } + + /** + * 对 [updateFeed] 获取到得数据进行二次整理并更新到 feedItems + */ + private fun updateFeedItems(data: PgcFeedData) { + logger.fInfo { "update $pgcType feed items: [items: ${data.items.size}, ranks: ${data.ranks.size}]" } + val epList = mutableStateListOf() + epList.addAll(restSubItems) + epList.addAll(data.items) + + epList.chunked(5).forEach { chunkedVCardList -> + if (chunkedVCardList.size == 5) { + feedItems.add( + FeedListItem( + type = FeedListType.Ep, + items = chunkedVCardList + ) + ) + } else { + restSubItems.clear() + restSubItems.addAll(chunkedVCardList) + } + } + + data.ranks.forEach { rank -> + feedItems.add( + FeedListItem( + type = FeedListType.Rank, + rank = rank + ) + ) + } + } +} + +data class FeedListItem( + val type: FeedListType, + val items: List? = emptyList(), + val rank: PgcFeedData.FeedRank? = null +) + +enum class FeedListType { + Ep, Rank +} \ No newline at end of file diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcCarouselData.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcCarouselData.kt new file mode 100644 index 00000000..fe8ecb71 --- /dev/null +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcCarouselData.kt @@ -0,0 +1,40 @@ +package dev.aaa1115910.biliapi.entity.pgc + +import dev.aaa1115910.biliapi.http.entity.pgc.PgcWebInitialStateData +import io.ktor.http.Url + +data class PgcCarouselData( + val items: List +) { + companion object { + fun fromPgcWebInitialStateData(data: PgcWebInitialStateData): PgcCarouselData { + val result = mutableListOf() + var isMovie = false + // 电影板块里的轮播图数据里没有直接包含 episodeId 和 seasonId + if (data.modules.banner.moduleId == 1668) isMovie = true + data.modules.banner.items.filter { + it.episodeId != null || (isMovie && it.link.contains("bangumi/play/ep")) + }.forEach { + var cover = it.bigCover ?: it.cover + if (cover.startsWith("//")) cover = "https:$cover" + result.add( + CarouselItem( + cover = cover, + title = it.title, + seasonId = it.seasonId ?: -1, + episodeId = it.episodeId + ?: Url(it.link).pathSegments.last().substring(2).toInt() + ) + ) + } + return PgcCarouselData(result) + } + } + + data class CarouselItem( + val cover: String, + val title: String, + val seasonId: Int, + val episodeId: Int + ) +} diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcFeedData.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcFeedData.kt new file mode 100644 index 00000000..261faed1 --- /dev/null +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcFeedData.kt @@ -0,0 +1,88 @@ +package dev.aaa1115910.biliapi.entity.pgc + +import dev.aaa1115910.biliapi.http.SeasonIndexType + +data class PgcFeedData( + var hasNext: Boolean, + var cursor: Int, + var items: List = emptyList(), + var ranks: List = emptyList() +) { + companion object { + fun fromPgcFeedData(data: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData): PgcFeedData { + return PgcFeedData( + hasNext = data.hasNext, + cursor = data.coursor, + items = data.items.map { FeedItem.fromFeedSubItem(it) }, + ranks = emptyList() + ) + } + + fun fromPgcFeedData(data: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data): PgcFeedData { + val itemsList = data.items.find { it.subItems.first().cardStyle == "v_card" } + val ranksList = data.items.find { it.subItems.first().cardStyle == "rank" } + return PgcFeedData( + hasNext = data.hasNext, + cursor = data.coursor, + items = itemsList?.subItems?.map { FeedItem.fromFeedSubItem(it) } ?: emptyList(), + ranks = ranksList?.subItems?.map { FeedRank.fromFeedSubItem(it) } ?: emptyList() + ) + } + } + + data class FeedItem( + var cover: String, + var title: String, + var subTitle: String, + var seasonId: Int, + var episodeId: Int, + var seasonType: SeasonIndexType, + var rating: String + ) { + companion object { + fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData.FeedSubItem): FeedItem { + return FeedItem( + cover = feedSubItem.cover, + title = feedSubItem.title, + subTitle = feedSubItem.subTitle, + seasonId = feedSubItem.seasonId!!, + episodeId = feedSubItem.episodeId, + seasonType = SeasonIndexType.fromId(feedSubItem.seasonType!!), + rating = feedSubItem.rating ?: "0" + ) + } + + fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data.FeedItem.FeedSubItem): FeedItem { + return FeedItem( + cover = feedSubItem.cover, + title = feedSubItem.title, + subTitle = feedSubItem.subTitle, + seasonId = feedSubItem.seasonId!!, + episodeId = feedSubItem.episodeId ?: feedSubItem.inline!!.epId, + seasonType = SeasonIndexType.fromId(feedSubItem.seasonType!!), + rating = feedSubItem.rating ?: "0" + ) + } + } + } + + data class FeedRank( + var cover: String, + var title: String, + var subTitle: String, + var items: List + ) { + companion object { + fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data.FeedItem.FeedSubItem): FeedRank { + return FeedRank( + cover = feedSubItem.cover, + title = feedSubItem.title, + subTitle = feedSubItem.subTitle, + items = feedSubItem.subItems?.map { FeedItem.fromFeedSubItem(it) } + ?: emptyList() + ) + } + } + } +} + diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApi.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApi.kt index f3ee8a34..7376e097 100644 --- a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApi.kt +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApi.kt @@ -3,11 +3,6 @@ package dev.aaa1115910.biliapi.http import com.tfowl.ktor.client.plugins.JsoupPlugin import dev.aaa1115910.biliapi.http.entity.BiliResponse import dev.aaa1115910.biliapi.http.entity.BiliResponseWithoutData -import dev.aaa1115910.biliapi.http.entity.anime.AnimeFeedData -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageData -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageDataType -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageDataV1 -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageDataV2 import dev.aaa1115910.biliapi.http.entity.danmaku.DanmakuData import dev.aaa1115910.biliapi.http.entity.danmaku.DanmakuResponse import dev.aaa1115910.biliapi.http.entity.dynamic.DynamicData @@ -15,6 +10,9 @@ import dev.aaa1115910.biliapi.http.entity.history.HistoryData import dev.aaa1115910.biliapi.http.entity.home.RcmdIndexData import dev.aaa1115910.biliapi.http.entity.home.RcmdTopData import dev.aaa1115910.biliapi.http.entity.index.IndexResultData +import dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData +import dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data +import dev.aaa1115910.biliapi.http.entity.pgc.PgcWebInitialStateData import dev.aaa1115910.biliapi.http.entity.search.AppSearchSquareData import dev.aaa1115910.biliapi.http.entity.search.KeywordSuggest import dev.aaa1115910.biliapi.http.entity.search.SearchResultData @@ -60,6 +58,7 @@ import dev.aaa1115910.biliapi.http.entity.video.VideoShot import dev.aaa1115910.biliapi.http.entity.web.NavResponseData import dev.aaa1115910.biliapi.http.util.BiliAppConf import dev.aaa1115910.biliapi.http.util.encApiSign +import dev.aaa1115910.biliapi.repositories.PgcType import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.okhttp.OkHttp @@ -1177,50 +1176,49 @@ object BiliHttpApi { }.body() /** 获取番剧首页数据 */ - suspend fun getAnimeHomepageData( - dataType: AnimeHomepageDataType = AnimeHomepageDataType.V1 - ): AnimeHomepageData? { - val htmlDocuments = client.get("https://www.bilibili.com/anime") { - when (dataType) { - AnimeHomepageDataType.V1 -> header("Cookie", "ogv_channel_version=v1") - AnimeHomepageDataType.V2 -> header("Cookie", "ogv_channel_version=v2") - } - }.body() + suspend fun getPgcWebInitialStateData(pgcType: PgcType): PgcWebInitialStateData { + val path = pgcType.name.lowercase() + val htmlDocuments = client.get("https://www.bilibili.com/$path").body() val dataScriptTagContent = htmlDocuments.body().select("script").find { it.html().contains("__INITIAL_STATE__") - }?.html() ?: return null + }?.html() ?: throw IllegalStateException("initial state data cannot be null") val dataJson = dataScriptTagContent.split("__INITIAL_STATE__=", ";(function()")[1] - - return when (dataType) { - AnimeHomepageDataType.V1 -> { - val dataV1 = - runCatching { json.decodeFromString(dataJson) }.getOrNull() - AnimeHomepageData(_dataV1 = dataV1) - } - - AnimeHomepageDataType.V2 -> { - val dataV2 = - runCatching { json.decodeFromString(dataJson) }.getOrNull() - AnimeHomepageData(_dataV2 = dataV2) - } - } + val initinalData = runCatching { + json.decodeFromString(dataJson) + }.onFailure { + println("parse initial state data failed: ${it.stackTraceToString()}") + }.getOrNull() ?: throw IllegalStateException("parse initial state data failed") + return initinalData } /** - * 获取猜你喜欢 + * 获取 PGC 猜你喜欢 * * 返回数据的前几条内包含每小时更新的分类排行榜 */ - suspend fun getAnimeFeed( + suspend fun getPgcFeedV3( name: String = "anime", cursor: Int = 0 - ): BiliResponse = client.get("/pgc/page/web/v3/feed") { + ): BiliResponse = client.get("/pgc/page/web/v3/feed") { parameter("name", name) parameter("coursor", cursor) }.body() + /** + * 获取 PGC 猜你喜欢 + */ + suspend fun getPgcFeed( + name: String = "movie", + cursor: Int = 0 + ): BiliResponse = client.get("/pgc/page/web/feed") { + parameter("name", name) + parameter("coursor", cursor) + parameter("new_cursor_status", true) + }.body() + + /** * 获取用户[mid]的追剧列表 * @@ -1555,7 +1553,11 @@ object BiliHttpApi { } enum class SeasonIndexType(val id: Int) { - Anime(1), Movie(2), Documentary(3), Guochuang(4), Tv(5), Variety(7) + Anime(1), Movie(2), Documentary(3), Guochuang(4), Tv(5), Variety(7); + + companion object { + fun fromId(id: Int) = entries.first { it.id == id } + } } private fun checkToken(accessKey: String?, sessData: String?) { diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageData.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageData.kt deleted file mode 100644 index ef9f26e6..00000000 --- a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageData.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.aaa1115910.biliapi.http.entity.anime - -import kotlinx.serialization.Serializable - -@Serializable -data class AnimeHomepageData( - private val _dataV1: AnimeHomepageDataV1? = null, - private val _dataV2: AnimeHomepageDataV2? = null -) { - fun getCarouselItems(): List { - val result = mutableListOf() - _dataV1?.carouselList?.forEach { - result.add( - CarouselItem( - cover = it.img, - title = it.title, - episodeId = it.blink.split("play\\ep", "?from")[1].toIntOrNull() - ) - ) - } - _dataV2?.modules?.banner?.items?.filter { it.episodeId != null }?.forEach { - result.add( - CarouselItem( - cover = it.bigCover, - title = it.title, - seasonId = it.seasonId - ) - ) - } - return result - } -} - -enum class AnimeHomepageDataType { - V1, V2 -} - -data class CarouselItem( - val cover: String, - val title: String, - val seasonId: Int? = null, - val episodeId: Int? = null -) \ No newline at end of file diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV1.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV1.kt deleted file mode 100644 index b9f8f7f1..00000000 --- a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV1.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.aaa1115910.biliapi.http.entity.anime - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement - -/** - * 动画首页数据(旧版) - * - * @param ver - * @param pageType - * @param carouselList 轮播推荐 - * @param handPickList 话题精选 - * @param handPickRecom 话题精选 - * @param tid - * @param showBv - */ -@Serializable -data class AnimeHomepageDataV1( - var ver: JsonElement?, - val pageType: Int, - val carouselList: List, - val handPickList: List, - val handPickRecom: List, - val tid: Int, - val showBv: Boolean -) - -@Serializable -data class AnimeHomepageDataItem( - val badge: String? = null, - val blink: String, - val gif: String, - val id: Int, - val img: String, - val index: Int? = null, - @SerialName("index_type") - val indexType: Int? = null, - @SerialName("index_value") - val indexValue: Int? = null, - val link: String, - val simg: String, - val title: String, - val wid: Int? = null -) \ No newline at end of file diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeed.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeed.kt new file mode 100644 index 00000000..00624f19 --- /dev/null +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeed.kt @@ -0,0 +1,49 @@ +package dev.aaa1115910.biliapi.http.entity.pgc + +import dev.aaa1115910.biliapi.http.entity.web.Hover +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray + +@Serializable +data class PgcFeedData( + @Suppress("SpellCheckingInspection") + var coursor: Int, + @SerialName("has_next") + val hasNext: Boolean, + var items: List = emptyList() +) { + @Serializable + data class FeedSubItem( + val cover: String, + @SerialName("episode_id") + val episodeId: Int, + val hover: Hover? = null, + val link: String? = null, + @SerialName("rank_id") + val rankId: Int, + val rating: String? = null, + @SerialName("season_id") + val seasonId: Int? = null, + @SerialName("season_type") + val seasonType: Int? = null, + val stat: Stat? = null, + @SerialName("sub_title") + val subTitle: String, + val text: JsonArray? = null, + val title: String, + val userStatus: UserStatus? = null + ) { + @Serializable + data class Stat( + val danmaku: Int, + val duration: Int, + val view: Long + ) + + @Serializable + data class UserStatus( + val follow: Int + ) + } +} \ No newline at end of file diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeFeed.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeedV3.kt similarity index 93% rename from bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeFeed.kt rename to bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeedV3.kt index d8b1c108..dd553178 100644 --- a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeFeed.kt +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeedV3.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.biliapi.http.entity.anime +package dev.aaa1115910.biliapi.http.entity.pgc import dev.aaa1115910.biliapi.http.entity.web.Hover import kotlinx.serialization.SerialName @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray @Serializable -data class AnimeFeedData( +data class PgcFeedV3Data( @Suppress("SpellCheckingInspection") var coursor: Int, @SerialName("has_next") @@ -26,6 +26,8 @@ data class AnimeFeedData( @SerialName("card_style") val cardStyle: String, val cover: String, + @SerialName("episode_id") + val episodeId: Int? = null, val evaluate: String? = null, val hover: Hover? = null, val inline: Inline? = null, @@ -48,7 +50,6 @@ data class AnimeFeedData( val text: JsonArray? = null, val title: String, val userStatus: UserStatus? = null - ) { @Serializable data class Inline( @@ -76,7 +77,7 @@ data class AnimeFeedData( data class Stat( val danmaku: Int, val duration: Int, - val view: Int + val view: Long ) @Serializable diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV2.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcWebInitialStateData.kt similarity index 86% rename from bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV2.kt rename to bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcWebInitialStateData.kt index 6f00a61c..3313dddb 100644 --- a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/anime/AnimeHomepageDataV2.kt +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcWebInitialStateData.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.biliapi.http.entity.anime +package dev.aaa1115910.biliapi.http.entity.pgc import dev.aaa1115910.biliapi.http.entity.web.Hover import kotlinx.serialization.SerialName @@ -7,10 +7,10 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement /** - * 动画首页数据(新版) + * PGC 首页 ssr 数据 */ @Serializable -data class AnimeHomepageDataV2( +data class PgcWebInitialStateData( val modules: Modules, ) { /** @@ -44,10 +44,10 @@ data class AnimeHomepageDataV2( val cover: String, val link: String, val evaluate: String? = null, - val report: JsonElement, + val report: JsonElement? = null, val hover: Hover? = null, val stat: Stat? = null, - val values: JsonArray, + val values: JsonArray? = null, @SerialName("season_id") val seasonId: Int? = null, @SerialName("season_type") @@ -57,23 +57,23 @@ data class AnimeHomepageDataV2( @SerialName("episode_id") val episodeId: Int? = null, @SerialName("big_cover") - val bigCover: String, + val bigCover: String? = null, @SerialName("play_btn") val playBtn: Int? = null, @SerialName("play_title") - val playTitle: String, + val playTitle: String? = null, @SerialName("rank_id") val rankId: Int, @SerialName("user_status") val userStatus: UserStatus? = null, @SerialName("date_ts") - val dateTs: Int, + val dateTs: Int? = null, @SerialName("day_of_week") - val dayOfWeek: Int, + val dayOfWeek: Int? = null, @SerialName("is_today") - val isToday: Int, + val isToday: Int? = null, @SerialName("is_latest") - val isLatest: Int, + val isLatest: Int? = null, val id: String, @SerialName("showReportData") val showReportData: ShowReportData, @@ -86,7 +86,7 @@ data class AnimeHomepageDataV2( @Serializable data class Stat( - val view: Int + val view: Long ) @Serializable diff --git a/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepository.kt b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepository.kt new file mode 100644 index 00000000..622d3f7e --- /dev/null +++ b/bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepository.kt @@ -0,0 +1,41 @@ +package dev.aaa1115910.biliapi.repositories + +import dev.aaa1115910.biliapi.entity.pgc.PgcCarouselData +import dev.aaa1115910.biliapi.entity.pgc.PgcFeedData +import dev.aaa1115910.biliapi.http.BiliHttpApi + +class PgcRepository { + suspend fun getCarousel(pgcType: PgcType): PgcCarouselData { + val initialStateData = BiliHttpApi.getPgcWebInitialStateData(pgcType) + val carouselData = PgcCarouselData.fromPgcWebInitialStateData(initialStateData) + return carouselData + } + + suspend fun getFeed(pgcType: PgcType, cursor: Int): PgcFeedData { + val data = when (pgcType) { + PgcType.Anime, PgcType.GuoChuang -> PgcFeedData.fromPgcFeedData( + BiliHttpApi.getPgcFeedV3( + name = pgcType.name.lowercase(), + cursor = cursor + ).getResponseData() + ) + + PgcType.Movie, PgcType.Tv, PgcType.Documentary, PgcType.Variety -> PgcFeedData.fromPgcFeedData( + BiliHttpApi.getPgcFeed( + name = pgcType.name.lowercase(), + cursor = cursor + ).getResponseData() + ) + } + return data + } +} + +enum class PgcType { + Anime, + GuoChuang, + Movie, + Documentary, + Tv, + Variety +} diff --git a/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApiTest.kt b/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApiTest.kt index 815a0473..9c5a532f 100644 --- a/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApiTest.kt +++ b/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApiTest.kt @@ -2,9 +2,9 @@ package dev.aaa1115910.biliapi.http import dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus import dev.aaa1115910.biliapi.entity.season.FollowingSeasonType -import dev.aaa1115910.biliapi.http.entity.anime.AnimeHomepageDataType import dev.aaa1115910.biliapi.http.entity.user.FollowAction import dev.aaa1115910.biliapi.http.entity.user.FollowActionSource +import dev.aaa1115910.biliapi.repositories.PgcType import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.Test @@ -559,18 +559,37 @@ internal class BiliHttpApiTest { } @Test - fun `get anime homepage data`() { + fun `get web initial state data`() { runBlocking { - AnimeHomepageDataType.values().forEach { - println(BiliHttpApi.getAnimeHomepageData(dataType = it)) + PgcType.entries.forEach { pgcType -> + println("type: ${pgcType.name}") + println( + BiliHttpApi.getPgcWebInitialStateData(pgcType) + .toString().replace("\n", "") + ) } } } @Test - fun `get anime feed data`() { + fun `get pgc feed data`() { runBlocking { - println(BiliHttpApi.getAnimeFeed()) + PgcType.entries.forEach { pgcType -> + println("type: ${pgcType.name}") + when (pgcType) { + PgcType.Anime, PgcType.GuoChuang -> + println( + BiliHttpApi.getPgcFeedV3(name = pgcType.name.lowercase()) + .toString().replace("\n", "") + ) + + PgcType.Tv, PgcType.Movie, PgcType.Documentary, PgcType.Variety -> + println( + BiliHttpApi.getPgcFeed(name = pgcType.name.lowercase()) + .toString().replace("\n", "") + ) + } + } } } diff --git a/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepositoryTest.kt b/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepositoryTest.kt new file mode 100644 index 00000000..15fe2eee --- /dev/null +++ b/bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepositoryTest.kt @@ -0,0 +1,33 @@ +package dev.aaa1115910.biliapi.repositories + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test + +class PgcRepositoryTest { + private val pgcRepository: PgcRepository = PgcRepository() + + @Test + fun `get pgc carousel data`() { + runBlocking { + PgcType.entries.forEach { pgcType -> + println("pgcType: $pgcType") + val data = pgcRepository.getCarousel(pgcType) + println(data) + } + } + } + + @Test + fun `get pgc feed data`() { + runBlocking { + PgcType.entries.forEach { pgcType -> + println("pgcType: $pgcType") + val data = pgcRepository.getFeed( + pgcType = pgcType, + cursor = 0 + ) + println(data) + } + } + } +} \ No newline at end of file