-修复搜索/动态/关注界面可以同时点赞/收藏同一个动态,使总点赞/收藏数增加 -修复个人主页上滑时动态/智能体/群聊图标会被遮挡 -评论显示不全,只显示50条内容(现在滑动到第50条评论后会出现加载更多按键) -评论筛选改为全部和最新和热门 -修复评论筛选图标自动发生改变
483 lines
14 KiB
Kotlin
483 lines
14 KiB
Kotlin
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)
|
||
}
|
||
|
||
} |