diff --git a/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt b/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt index ebab19f..daf4fec 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt @@ -32,6 +32,28 @@ data class Moment( val time: String, @SerializedName("isFollowed") val isFollowed: Boolean, + @SerializedName("isNews") + val isNews: Boolean? = null, + @SerializedName("newsTitle") + val newsTitle: String? = null, + @SerializedName("newsUrl") + val newsUrl: String? = null, + @SerializedName("newsSource") + val newsSource: String? = null, + @SerializedName("newsCategory") + val newsCategory: String? = null, + @SerializedName("newsLanguage") + val newsLanguage: String? = null, + @SerializedName("newsContent") + val newsContent: String? = null, + @SerializedName("hasFullText") + val hasFullText: Boolean? = null, + @SerializedName("summary") + val summary: String? = null, + @SerializedName("publishedAt") + val publishedAt: String? = null, + @SerializedName("imageCached") + val imageCached: Boolean? = null, ) { fun toMomentItem(): MomentEntity { return MomentEntity( @@ -60,6 +82,17 @@ data class Moment( authorId = user.id.toInt(), liked = isLiked, isFavorite = isFavorite, + isNews = isNews, + newsTitle = newsTitle, + newsUrl = newsUrl, + newsSource = newsSource, + newsCategory = newsCategory, + newsLanguage = newsLanguage, + newsContent = newsContent, + hasFullText = hasFullText, + summary = summary, + publishedAt = publishedAt, + imageCached = imageCached, ) } } diff --git a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt index 3498faa..a207829 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt @@ -316,6 +316,7 @@ interface RaveNowAPI { suspend fun getPosts( @Query("page") page: Int = 1, @Query("pageSize") pageSize: Int = 20, + @Query("id") postId: Int? = null, @Query("timelineId") timelineId: Int? = null, @Query("authorId") authorId: Int? = null, @Query("contentSearch") contentSearch: String? = null, @@ -323,6 +324,15 @@ interface RaveNowAPI { @Query("trend") trend: String? = null, @Query("favouriteUserId") favouriteUserId: Int? = null, @Query("explore") explore: String? = null, + @Query("imageTag") imageTag: String? = null, + @Query("search") search: String? = null, + @Query("advancedSearch") advancedSearch: String? = null, + @Query("newsFilter") newsFilter: String? = null, + @Query("onlyNews") onlyNews: Boolean? = null, + @Query("newsSource") newsSource: String? = null, + @Query("newsLanguage") newsLanguage: String? = null, + @Query("newsCategory") newsCategory: String? = null, + @Query("requireImageCache") requireImageCache: Boolean? = null, ): Response> @Multipart diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt b/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt index 38ecc6b..713b90c 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt @@ -299,12 +299,35 @@ data class MomentEntity( // 关联动态 var relMoment: MomentEntity? = null, // 是否收藏 - var isFavorite: Boolean = false + var isFavorite: Boolean = false, + // 是否为新闻 + val isNews: Boolean? = null, + // 新闻标题 + val newsTitle: String? = null, + // 新闻链接 + val newsUrl: String? = null, + // 新闻来源 + val newsSource: String? = null, + // 新闻分类 + val newsCategory: String? = null, + // 新闻语言 + val newsLanguage: String? = null, + // 新闻内容 + val newsContent: String? = null, + // 是否有完整文本 + val hasFullText: Boolean? = null, + // 摘要 + val summary: String? = null, + // 发布时间 + val publishedAt: String? = null, + // 图片是否已缓存 + val imageCached: Boolean? = null ) class MomentLoaderExtraArgs( val explore: Boolean? = false, val timelineId: Int? = null, - val authorId : Int? = null + val authorId : Int? = null, + val newsOnly: Boolean? = false ) class MomentLoader : DataLoader() { override suspend fun fetchData( @@ -317,7 +340,8 @@ class MomentLoader : DataLoader() { pageSize = pageSize, explore = if (extra.explore == true) "true" else "", timelineId = extra.timelineId, - authorId = extra.authorId + authorId = extra.authorId, + newsFilter = if (extra.newsOnly == true) "news_only" else "" ) val data = result.body()?.let { ListContainer( diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt index d430052..50c7571 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt @@ -44,6 +44,7 @@ import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.Dynamic import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList +import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.News import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel import com.aiosman.ravenow.ui.modifiers.noRippleClickable @@ -66,8 +67,8 @@ fun MomentsList() { val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() - // 游客模式下不显示timeline,只显示2个tab:Dynamic、Hot // 游客模式下不显示timeline,只显示3个tab:Explore、Dynamic、Hot - val tabCount = if (AppStore.isGuest) 2 else 3 // val tabCount = if (AppStore.isGuest) 3 else 4 + // 游客模式下显示3个tab:Worldwide、Hot、News,非游客模式显示4个tab:Worldwide、Following、Hot、News + val tabCount = if (AppStore.isGuest) 3 else 4 var pagerState = rememberPagerState { tabCount } var scope = rememberCoroutineScope() Column( @@ -204,6 +205,23 @@ fun MomentsList() { } ) } + + TabSpacer() + + // 新闻标签 + Box { + CustomTabItem( + text = stringResource(R.string.index_news), + isSelected = pagerState.currentPage == 3, + onClick = { + tabDebouncer { + scope.launch { + pagerState.animateScrollToPage(3) + } + } + } + ) + } } else { // 热门标签 (游客模式) Box { @@ -219,6 +237,23 @@ fun MomentsList() { } ) } + + TabSpacer() + + // 新闻标签 (游客模式) + Box { + CustomTabItem( + text = stringResource(R.string.index_news), + isSelected = pagerState.currentPage == 2, + onClick = { + tabDebouncer { + scope.launch { + pagerState.animateScrollToPage(2) + } + } + } + ) + } } } @@ -229,7 +264,7 @@ fun MomentsList() { .weight(1f) ) { if (AppStore.isGuest) { - // 游客模式:Dynamic(0), Hot(1) + // 游客模式:Worldwide(0), Hot(1), News(2) when (it) { 0 -> { Dynamic() @@ -237,9 +272,12 @@ fun MomentsList() { 1 -> { HotMomentsList() } + 2 -> { + News() + } } } else { - // 正常用户:Dynamic(0), Timeline(1), Hot(2) + // 正常用户:Worldwide(0), Following(1), Hot(2), News(3) when (it) { 0 -> { Dynamic() @@ -250,6 +288,9 @@ fun MomentsList() { 2 -> { HotMomentsList() } + 3 -> { + News() + } } } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/News.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/News.kt new file mode 100644 index 0000000..008ee53 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/News.kt @@ -0,0 +1,179 @@ +package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.aiosman.ravenow.GuestLoginCheckOut +import com.aiosman.ravenow.GuestLoginCheckOutScene +import com.aiosman.ravenow.LocalNavController +import com.aiosman.ravenow.ui.NavigationRoute +import com.aiosman.ravenow.ui.composables.MomentCard +import com.aiosman.ravenow.ui.composables.rememberDebouncer +import kotlinx.coroutines.launch + +/** + * 新闻动态列表 + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun News() { + val model = NewsViewModel + val moments = model.moments + val navController = LocalNavController.current + + val scope = rememberCoroutineScope() + val state = rememberPullRefreshState(model.refreshing, onRefresh = { + model.refreshPager( + pullRefresh = true + ) + }) + val listState = rememberLazyListState() + + // 用于跟踪是否已经触发过加载更多 + var hasTriggeredLoadMore by remember { mutableStateOf(false) } + + // observe list scrolling with simplified logic + val reachedBottom by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() + val totalItems = layoutInfo.totalItemsCount + + if (lastVisibleItem == null || totalItems == 0) { + false + } else { + val isLastItemVisible = lastVisibleItem.index >= totalItems - 2 + + // 简化逻辑:只要滑动到底部且还没有触发过,就触发加载 + isLastItemVisible && !hasTriggeredLoadMore + } + } + } + + // load more if scrolled to bottom + LaunchedEffect(reachedBottom) { + if (reachedBottom) { + hasTriggeredLoadMore = true + model.loadMore() + } + } + LaunchedEffect(Unit) { + model.refreshPager() + } + + // 监听数据变化,重置加载状态 + LaunchedEffect(moments.size) { + if (moments.size > 0) { + // 延迟重置,确保数据已经稳定 + kotlinx.coroutines.delay(500) + hasTriggeredLoadMore = false + } + } + Column( + modifier = Modifier + .fillMaxSize() + + ) { + Box(Modifier.pullRefresh(state)) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState + ) { + items( + moments.size, + key = { idx -> idx } + ) { idx -> + //处理下标越界 + if (idx < 0 || idx >= moments.size) return@items + val momentItem = moments[idx] ?: return@items + + val commentDebouncer = rememberDebouncer() + val likeDebouncer = rememberDebouncer() + val favoriteDebouncer = rememberDebouncer() + val followDebouncer = rememberDebouncer() + + MomentCard( + momentEntity = momentItem, + onAddComment = { + commentDebouncer { + // 检查游客模式,如果是游客则跳转登录 + if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) { + navController.navigate(NavigationRoute.Login.route) + } else { + scope.launch { + model.onAddComment(momentItem.id) + } + } + } + }, + onLikeClick = { + likeDebouncer { + // 检查游客模式,如果是游客则跳转登录 + if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { + navController.navigate(NavigationRoute.Login.route) + } else { + scope.launch { + if (momentItem.liked) { + model.dislikeMoment(momentItem.id) + } else { + model.likeMoment(momentItem.id) + } + } + } + } + }, + onFavoriteClick = { + favoriteDebouncer { + // 检查游客模式,如果是游客则跳转登录 + if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { + navController.navigate(NavigationRoute.Login.route) + } else { + scope.launch { + if (momentItem.isFavorite) { + model.unfavoriteMoment(momentItem.id) + } else { + model.favoriteMoment(momentItem.id) + } + } + } + } + }, + onFollowClick = { + followDebouncer { + // 检查游客模式,如果是游客则跳转登录 + if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.FOLLOW_USER)) { + navController.navigate(NavigationRoute.Login.route) + } else { + model.followAction(momentItem) + } + } + }, + showFollowButton = true + ) + } + } + + PullRefreshIndicator( + refreshing = model.refreshing, + state = state, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsViewModel.kt new file mode 100644 index 0000000..136e7b1 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsViewModel.kt @@ -0,0 +1,16 @@ +package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news + +import com.aiosman.ravenow.entity.MomentLoaderExtraArgs +import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel +import org.greenrobot.eventbus.EventBus + + +object NewsViewModel : BaseMomentModel() { + init { + EventBus.getDefault().register(this) + } + + override fun extraArgs(): MomentLoaderExtraArgs { + return MomentLoaderExtraArgs(explore = true, newsOnly = true) + } +} diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 18d4cee..0cb9c80 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -26,4 +26,11 @@ AI美化アイコン 編集アイコン アバター選択 + + + ワールドワイド + ダイナミック + フォロー中 + ホット + ニュース diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c562a63..a29cf75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,6 +122,7 @@ Dynamic Following Hot + News Home Agent Message