Files
rider-pro-android-app/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt
高帆 8937ccbf56 修复多个bug、更改评论筛选
-修复搜索/动态/关注界面可以同时点赞/收藏同一个动态,使总点赞/收藏数增加
-修复个人主页上滑时动态/智能体/群聊图标会被遮挡
-评论显示不全,只显示50条内容(现在滑动到第50条评论后会出现加载更多按键)
-评论筛选改为全部和最新和热门
-修复评论筛选图标自动发生改变
2025-11-27 18:38:18 +08:00

483 lines
14 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.aiosman.ravenow.entity
import androidx.annotation.DrawableRes
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.api.AgentMomentRequestBody
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.parseErrorResponse
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.IOException
import java.util.Date
/**
* 动态分页加载器
*/
class MomentPagingSource(
private val remoteDataSource: MomentRemoteDataSource,
private val author: Int? = null,
private val timelineId: Int? = null,
private val contentSearch: String? = null,
private val trend: Boolean? = false,
private val explore: Boolean? = false,
private val favoriteUserId: Int? = null
) : PagingSource<Int, MomentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MomentEntity> {
return try {
val currentPage = params.key ?: 1
val moments = remoteDataSource.getMoments(
pageNumber = currentPage,
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
explore = explore,
favoriteUserId = favoriteUserId
)
LoadResult.Page(
data = moments.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (moments.list.isEmpty()) null else moments.page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, MomentEntity>): Int? {
return state.anchorPosition
}
}
class MomentRemoteDataSource(
private val momentService: MomentService,
) {
suspend fun getMoments(
pageNumber: Int,
author: Int?,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
explore: Boolean?,
favoriteUserId: Int?
): ListContainer<MomentEntity> {
return momentService.getMoments(
pageNumber = pageNumber,
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
explore = explore,
favoriteUserId = favoriteUserId
)
}
}
class MomentServiceImpl() : MomentService {
val momentBackend = MomentBackend()
override suspend fun getMoments(
pageNumber: Int,
author: Int?,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
explore: Boolean?,
favoriteUserId: Int?,
): ListContainer<MomentEntity> {
return momentBackend.fetchMomentItems(
pageNumber = pageNumber,
author = author,
timelineId = timelineId,
contentSearch = contentSearch,
trend = trend,
favoriteUserId = favoriteUserId,
explore = explore
)
}
override suspend fun getMomentById(id: Int): MomentEntity {
return momentBackend.getMomentById(id)
}
override suspend fun likeMoment(id: Int) {
momentBackend.likeMoment(id)
}
override suspend fun dislikeMoment(id: Int) {
momentBackend.dislikeMoment(id)
}
override suspend fun createMoment(
content: String,
authorId: Int,
images: List<UploadImage>,
relPostId: Int?
): MomentEntity {
return momentBackend.createMoment(content, authorId, images, relPostId)
}
override suspend fun agentMoment(content: String): String {
return momentBackend.agentMoment(content)
}
override suspend fun favoriteMoment(id: Int) {
momentBackend.favoriteMoment(id)
}
override suspend fun unfavoriteMoment(id: Int) {
momentBackend.unfavoriteMoment(id)
}
override suspend fun deleteMoment(id: Int) {
momentBackend.deleteMoment(id)
}
}
class MomentBackend {
val DataBatchSize = 20
suspend fun fetchMomentItems(
pageNumber: Int,
author: Int? = null,
timelineId: Int?,
contentSearch: String?,
trend: Boolean?,
explore: Boolean?,
favoriteUserId: Int? = null
): ListContainer<MomentEntity> {
val resp = ApiClient.api.getPosts(
pageSize = DataBatchSize,
page = pageNumber,
timelineId = timelineId,
authorId = author,
contentSearch = contentSearch,
trend = if (trend == true) "true" else "",
favouriteUserId = favoriteUserId,
explore = if (explore == true) "true" else ""
)
val body = resp.body() ?: throw ServiceException("Failed to get moments")
return ListContainer(
total = body.total,
page = pageNumber,
pageSize = DataBatchSize,
list = body.list.map { it.toMomentItem() }
)
}
suspend fun getMomentById(id: Int): MomentEntity {
var resp = ApiClient.api.getPost(id)
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to get moment")
}
return resp.body()?.data?.toMomentItem() ?: throw ServiceException("Failed to get moment")
}
suspend fun likeMoment(id: Int) {
ApiClient.api.likePost(id)
}
suspend fun dislikeMoment(id: Int) {
ApiClient.api.dislikePost(id)
}
fun createMultipartBody(file: File, name: String): MultipartBody.Part {
val requestFile = RequestBody.create("image/*".toMediaTypeOrNull(), file)
return MultipartBody.Part.createFormData(name, file.name, requestFile)
}
suspend fun createMoment(
content: String,
authorId: Int,
imageUriList: List<UploadImage>,
relPostId: Int?
): MomentEntity {
val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
val imageList = imageUriList.map { item ->
val file = item.file
createMultipartBody(file, "image")
}
val response = ApiClient.api.createPost(imageList, textContent = textContent)
val body = response.body()?.data ?: throw ServiceException("Failed to create moment")
return body.toMomentItem()
}
suspend fun agentMoment(
content: String,
): String {
val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
val sessionId = ""
val response = ApiClient.api.agentMoment(AgentMomentRequestBody(generateText = content, sessionId =sessionId ))
val body = response.body()?.data ?: throw ServiceException("Failed to agent moment")
return body.toString()
}
suspend fun favoriteMoment(id: Int) {
ApiClient.api.favoritePost(id)
}
suspend fun unfavoriteMoment(id: Int) {
ApiClient.api.unfavoritePost(id)
}
suspend fun deleteMoment(id: Int) {
ApiClient.api.deletePost(id)
}
}
/**
* 动态图片
*/
data class MomentImageEntity(
// 图片ID
val id: Long,
// 图片URL
val url: String,
// 原始图片URL
val originalUrl: String? = null,
// 直接访问URL
val directUrl: String? = null,
// 缩略图URL
val thumbnail: String,
// 缩略图直接访问URL
val thumbnailDirectUrl: String? = null,
// 小尺寸图片URL
val small: String? = null,
// 小尺寸图片直接访问URL
val smallDirectUrl: String? = null,
// 中尺寸图片URL
val medium: String? = null,
// 中尺寸图片直接访问URL
val mediumDirectUrl: String? = null,
// 大尺寸图片URL
val large: String? = null,
// 大尺寸图片直接访问URL
val largeDirectUrl: String? = null,
// 图片BlurHash
val blurHash: String? = null,
// 宽度
var width: Int? = null,
// 高度
var height: Int? = null
)
/**
* 动态视频
*/
data class MomentVideoEntity(
// 视频ID
val id: Long,
// 视频URL
val url: String,
// 原始文件名
val originalUrl: String? = null,
// 直接访问URL
val directUrl: String? = null,
// 视频缩略图URL
val thumbnailUrl: String? = null,
// 视频缩略图直接访问URL
val thumbnailDirectUrl: String? = null,
// 视频时长(秒)
val duration: Int? = null,
// 宽度
val width: Int? = null,
// 高度
val height: Int? = null,
// 文件大小(字节)
val size: Long? = null,
// 视频格式
val format: String? = null,
// 视频比特率kbps
val bitrate: Int? = null,
// 帧率
val frameRate: String? = null
)
/**
* 动态
*/
data class MomentEntity(
// 动态ID
val id: Int,
// 作者头像
val avatar: String,
// 作者昵称
val nickname: String,
// 区域
val location: String,
// 动态时间
val time: Date,
// 是否关注
val followStatus: Boolean,
// 动态内容
val momentTextContent: String?,
// 动态图片
@DrawableRes val momentPicture: Int,
// 点赞数
val likeCount: Int,
// 评论数
val commentCount: Int,
// 分享数
val shareCount: Int,
// 收藏数
val favoriteCount: Int,
// 动态图片列表
val images: List<MomentImageEntity> = emptyList(),
// 作者ID
val authorId: Int = 0,
// 是否点赞
var liked: Boolean = false,
// 关联动态ID
var relPostId: Int? = null,
// 关联动态
var relMoment: MomentEntity? = null,
// 是否收藏
var isFavorite: Boolean = false,
// 外部链接
val url: String? = null,
// 动态视频列表
val videos: List<MomentVideoEntity>? = null,
// 新闻相关字段
val isNews: Boolean = false,
val newsTitle: String = "",
val newsUrl: String = "",
val newsSource: String = "",
val newsCategory: String = "",
val newsLanguage: String = "",
val newsContent: String = "",
// 是否已获取完整正文
val hasFullText: Boolean = false,
// 新闻摘要
val summary: String? = null,
// 新闻发布时间
val publishedAt: String? = null,
// 是否已缓存图片
val imageCached: Boolean = false
)
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null,
val newsOnly: Boolean? = null,
val videoOnly: Boolean? = null
)
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData(
page: Int,
pageSize: Int,
extra: MomentLoaderExtraArgs
): ListContainer<MomentEntity> {
val result = ApiClient.api.getPosts(
page = page,
pageSize = pageSize,
explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId,
authorId = extra.authorId,
newsFilter = if (extra.newsOnly == true) "news_only" else "",
videoFilter = if (extra.videoOnly == true) "video_only" else ""
)
val data = result.body()?.let {
ListContainer(
list = it.list.map { it.toMomentItem() },
total = it.total,
page = page,
pageSize = pageSize
)
}
if (data == null) {
throw ServiceException("Failed to get moments")
}
return data
}
fun updateMomentLike(id: Int,isLike:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.liked != isLike) {
if (isLike) 1 else -1
} else {
0
}
momentItem.copy(
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
liked = isLike
)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.isFavorite != isFavorite) {
if (isFavorite) 1 else -1
} else {
0
}
momentItem.copy(
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
isFavorite = isFavorite
)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun updateCommentCount(id: Int, delta: Int) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
momentItem.copy(commentCount = newCount)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun removeMoment(id: Int) {
this.list = this.list.filter { it.id != id }.toMutableList()
onListChanged?.invoke(this.list)
}
fun addMoment(moment: MomentEntity) {
this.list.add(0, moment)
onListChanged?.invoke(this.list)
}
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
}