- 新增推荐Tab,采用垂直滑动样式,展示推荐动态内容。 - 推荐Tab支持预加载周围图片,提升滑动体验,并增加loading和错误状态指示。 - 优化评论弹窗UI,移除自动聚焦,调整背景色和输入框样式。 - 动态Tab样式调整,使用下划线指示当前选中Tab。 - 调整MomentLoaderExtraArgs,增加trend参数用于推荐动态加载。 - 新增字符串资源 `index_recommend`。
406 lines
12 KiB
Kotlin
406 lines
12 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 thumbnail: String,
|
|
// 图片BlurHash
|
|
val blurHash: String? = null,
|
|
// 宽度
|
|
var width: Int? = null,
|
|
// 高度
|
|
var height: Int? = 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 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 newsOnly: Boolean? = false,
|
|
val trend: Boolean? = false
|
|
)
|
|
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 "",
|
|
trend = if (extra.trend == true) "1" 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) {
|
|
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, 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) {
|
|
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
|
} 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)
|
|
}
|
|
|
|
} |