更新动态加载逻辑

This commit is contained in:
2024-12-01 09:40:13 +08:00
parent 6c19f83cfb
commit 79fccda1aa
25 changed files with 657 additions and 524 deletions

View File

@@ -116,6 +116,8 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.1") // 添加 SplashScreen 依赖 implementation("androidx.core:core-splashscreen:1.0.1") // 添加 SplashScreen 依赖
// 添加 lifecycle-runtime-ktx 依赖 // 添加 lifecycle-runtime-ktx 依赖
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation ("org.greenrobot:eventbus:3.3.1")
} }

View File

@@ -17,6 +17,7 @@ import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
import com.aiosman.ravenow.ui.follower.FollowingListViewModel import com.aiosman.ravenow.ui.follower.FollowingListViewModel
import com.aiosman.ravenow.ui.index.IndexViewModel import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.MomentExploreViewModel
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.index.tabs.search.DiscoverViewModel import com.aiosman.ravenow.ui.index.tabs.search.DiscoverViewModel
@@ -131,6 +132,7 @@ object AppState {
fun ReloadAppState(context: Context) { fun ReloadAppState(context: Context) {
// 重置动态列表页面 // 重置动态列表页面
TimelineMomentViewModel.ResetModel() TimelineMomentViewModel.ResetModel()
MomentExploreViewModel.ResetModel()
// 重置我的页面 // 重置我的页面
MyProfileViewModel.ResetModel() MyProfileViewModel.ResetModel()
// 重置发现页面 // 重置发现页面

View File

@@ -0,0 +1,60 @@
package com.aiosman.ravenow.entity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.aiosman.ravenow.data.ListContainer
abstract class DataLoader<T,ET> {
var list: MutableList<T> = mutableListOf()
var page by mutableStateOf(1)
var total by mutableStateOf(0)
var pageSize by mutableStateOf(10)
var hasNext by mutableStateOf(true)
var onListChanged: ((List<T>) -> Unit)? = null
private var firstLoad = true
abstract suspend fun fetchData(
page: Int, pageSize: Int, extra: ET
): ListContainer<T>
suspend fun loadData(
extra: ET
) {
if (!firstLoad) {
return
}
firstLoad = false
val result = fetchData(page, pageSize, extra)
list = result.list.toMutableList()
this.page = page
this.total = result.total
this.pageSize = pageSize
this.hasNext = result.list.size == pageSize
onListChanged?.invoke(list)
}
suspend fun loadMore(extra: ET) {
if (firstLoad) {
return
}
if (!hasNext) {
return
}
val result = fetchData(page + 1, pageSize, extra)
list.addAll(result.list)
page += 1
hasNext = result.list.size == pageSize
onListChanged?.invoke(list)
}
fun clear() {
list.clear()
page = 1
total = 0
pageSize = 10
hasNext = true
firstLoad = true
}
}

View File

@@ -285,3 +285,68 @@ data class MomentEntity(
// 是否收藏 // 是否收藏
var isFavorite: Boolean = false var isFavorite: Boolean = false
) )
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = 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
)
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)
}
}

View File

@@ -0,0 +1,7 @@
package com.aiosman.ravenow.event
import com.aiosman.ravenow.entity.MomentEntity
data class MomentAddEvent(
val moment:MomentEntity
)

View File

@@ -0,0 +1,6 @@
package com.aiosman.ravenow.event
data class MomentFavouriteChangeEvent(
val postId: Int,
val isFavourite: Boolean
)

View File

@@ -0,0 +1,7 @@
package com.aiosman.ravenow.event
data class MomentLikeChangeEvent(
val postId: Int,
val likeCount: Int?,
val isLike: Boolean
)

View File

@@ -0,0 +1,5 @@
package com.aiosman.ravenow.event
data class MomentRemoveEvent(
val postId: Int
)

View File

@@ -66,6 +66,7 @@ import com.aiosman.ravenow.ui.navigateToPost
@Composable @Composable
fun MomentCard( fun MomentCard(
modifier: Modifier = Modifier,
momentEntity: MomentEntity, momentEntity: MomentEntity,
onLikeClick: () -> Unit = {}, onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}, onFavoriteClick: () -> Unit = {},
@@ -78,7 +79,7 @@ fun MomentCard(
var imageIndex by remember { mutableStateOf(0) } var imageIndex by remember { mutableStateOf(0) }
val navController = LocalNavController.current val navController = LocalNavController.current
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .background(AppColors.background)
) { ) {

View File

@@ -0,0 +1,129 @@
package com.aiosman.ravenow.ui.index.tabs.moment
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentLoader
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
open class BaseMomentModel :ViewModel(){
private val momentService: MomentService = MomentServiceImpl()
private val userService = UserServiceImpl()
val momentLoader = MomentLoader().apply {
onListChanged = {
moments = it
}
}
var refreshing by mutableStateOf(false)
var isFirstLoad = true
var moments by mutableStateOf<List<MomentEntity>>(listOf())
open fun extraArgs(): MomentLoaderExtraArgs {
return MomentLoaderExtraArgs()
}
fun refreshPager(pullRefresh: Boolean = false) {
if (!isFirstLoad && !pullRefresh) {
return
}
isFirstLoad = false
momentLoader.clear()
viewModelScope.launch {
momentLoader.loadData(extraArgs())
}
}
fun loadMore() {
viewModelScope.launch {
momentLoader.loadMore(extraArgs())
moments = momentLoader.list
}
}
@Subscribe
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
momentLoader.updateMomentLike(event.postId, event.isLike)
}
suspend fun likeMoment(id: Int) {
momentService.likeMoment(id)
momentLoader.updateMomentLike(id, true)
}
suspend fun dislikeMoment(id: Int) {
momentService.dislikeMoment(id)
momentLoader.updateMomentLike(id, false)
}
suspend fun onAddComment(id: Int) {
// val currentPagingData = _momentsFlow.value
// updateCommentCount(id)
}
@Subscribe
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)
}
suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id)
momentLoader.updateFavoriteCount(id, false)
}
@Subscribe
fun onRemoveMomentEvent(event: MomentRemoveEvent) {
momentLoader.removeMoment(event.postId)
}
@Subscribe
fun onAddMomentEvent(event: MomentAddEvent) {
momentLoader.addMoment(event.moment)
}
fun followAction(moment: MomentEntity) {
// viewModelScope.launch {
// try {
// if (moment.followStatus) {
// userService.unFollowUser(moment.authorId.toString())
// } else {
// userService.followUser(moment.authorId.toString())
// }
// updateFollowStatus(moment.authorId, !moment.followStatus)
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
}
fun ResetModel() {
momentLoader.clear()
isFirstLoad = true
}
override fun onCleared() {
super.onCleared()
EventBus.getDefault().unregister(this)
}
}

View File

@@ -4,16 +4,19 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.ravenow.ui.composables.MomentCard import com.aiosman.ravenow.ui.composables.MomentCard
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -24,14 +27,30 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun ExploreMomentsList() { fun ExploreMomentsList() {
val model = MomentExploreViewModel val model = MomentExploreViewModel
var dataFlow = model.momentsFlow var moments = model.moments
var moments = dataFlow.collectAsLazyPagingItems()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(model.refreshing, onRefresh = { val state = rememberPullRefreshState(model.refreshing, onRefresh = {
model.refreshPager( model.refreshPager(
pullRefresh = true pullRefresh = true
) )
}) })
val listState = rememberLazyListState()
// observe list scrolling
val reachedBottom by remember {
derivedStateOf {
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
}
}
// load more if scrolled to bottom
LaunchedEffect(reachedBottom) {
if (reachedBottom) {
model.loadMore()
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.refreshPager() model.refreshPager()
} }
@@ -43,9 +62,10 @@ fun ExploreMomentsList() {
Box(Modifier.pullRefresh(state)) { Box(Modifier.pullRefresh(state)) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState
) { ) {
items( items(
moments.itemCount, moments.size,
key = { idx -> idx } key = { idx -> idx }
) { idx -> ) { idx ->
val momentItem = moments[idx] ?: return@items val momentItem = moments[idx] ?: return@items
@@ -75,7 +95,7 @@ fun ExploreMomentsList() {
}, },
onFollowClick = { onFollowClick = {
model.followAction(momentItem) model.followAction(momentItem)
}, }
) )
} }
} }

View File

@@ -0,0 +1,17 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
import org.greenrobot.eventbus.EventBus
object MomentExploreViewModel : BaseMomentModel() {
init {
EventBus.getDefault().register(this)
}
override fun extraArgs(): MomentLoaderExtraArgs {
return MomentLoaderExtraArgs(explore = true)
}
}

View File

@@ -1,182 +0,0 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
object MomentExploreViewModel : ViewModel() {
private val momentService: MomentService = MomentServiceImpl()
private val userService = UserServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
val momentsFlow = _momentsFlow.asStateFlow()
var existsMoment = mutableStateOf(false)
var refreshing by mutableStateOf(false)
var isFirstLoad = true
fun refreshPager(pullRefresh: Boolean = false) {
if (!isFirstLoad && !pullRefresh) {
return
}
isFirstLoad = false
viewModelScope.launch {
if (pullRefresh) {
refreshing = true
}
// 检查是否有动态
val existMoments =
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
if (existMoments.list.isEmpty()) {
existsMoment.value = true
}
if (pullRefresh) {
refreshing = false
}
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
// 如果没有动态,则显示热门动态
explore = true
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it
}
}
}
fun updateLikeCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun likeMoment(id: Int) {
momentService.likeMoment(id)
updateLikeCount(id)
}
fun updateCommentCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(commentCount = momentItem.commentCount + 1)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun onAddComment(id: Int) {
val currentPagingData = _momentsFlow.value
updateCommentCount(id)
}
fun updateDislikeMomentById(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun dislikeMoment(id: Int) {
momentService.dislikeMoment(id)
updateDislikeMomentById(id)
}
fun updateFavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
updateFavoriteCount(id)
}
fun updateUnfavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id)
updateUnfavoriteCount(id)
}
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun followAction(moment: MomentEntity) {
viewModelScope.launch {
try {
if (moment.followStatus) {
userService.unFollowUser(moment.authorId.toString())
} else {
userService.followUser(moment.authorId.toString())
}
updateFollowStatus(moment.authorId, !moment.followStatus)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun ResetModel() {
_momentsFlow.value = PagingData.empty()
isFirstLoad = true
}
}

View File

@@ -4,12 +4,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -24,8 +28,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun TimelineMomentsList() { fun TimelineMomentsList() {
val model = TimelineMomentViewModel val model = TimelineMomentViewModel
var dataFlow = model.momentsFlow var moments = model.moments
var moments = dataFlow.collectAsLazyPagingItems()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(model.refreshing, onRefresh = { val state = rememberPullRefreshState(model.refreshing, onRefresh = {
model.refreshPager( model.refreshPager(
@@ -35,6 +38,20 @@ fun TimelineMomentsList() {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.refreshPager() model.refreshPager()
} }
val listState = rememberLazyListState()
// observe list scrolling
val reachedBottom by remember {
derivedStateOf {
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
}
}
LaunchedEffect(reachedBottom) {
if (reachedBottom) {
model.loadMore()
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -42,12 +59,13 @@ fun TimelineMomentsList() {
Box(Modifier.pullRefresh(state)) { Box(Modifier.pullRefresh(state)) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState
) { ) {
items( items(
moments.itemCount, moments.size,
key = { idx -> moments[idx]?.id ?: idx } key = { idx -> moments[idx].id }
) { idx -> ) { idx ->
val momentItem = moments[idx] ?: return@items val momentItem = moments[idx]
MomentCard(momentEntity = momentItem, MomentCard(momentEntity = momentItem,
onAddComment = { onAddComment = {
scope.launch { scope.launch {
@@ -77,12 +95,6 @@ fun TimelineMomentsList() {
}, },
showFollowButton = false showFollowButton = false
) )
// Box(
// modifier = Modifier
// .height(4.dp)
// .fillMaxWidth()
// .background(Color(0xFFF0F2F5))
// )
} }
} }
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter)) PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))

View File

@@ -1,209 +0,0 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
object TimelineMomentViewModel : ViewModel() {
private val momentService: MomentService = MomentServiceImpl()
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
private val userService :UserService = UserServiceImpl()
val momentsFlow = _momentsFlow.asStateFlow()
var existsMoment = mutableStateOf(false)
var refreshing by mutableStateOf(false)
var isFirstLoad = true
fun refreshPager(pullRefresh: Boolean = false) {
if (!isFirstLoad && !pullRefresh) {
return
}
isFirstLoad = false
viewModelScope.launch {
if (pullRefresh) {
refreshing = true
}
// 检查是否有动态
val existMoments =
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
if (existMoments.list.isEmpty()) {
existsMoment.value = true
}
if (pullRefresh) {
refreshing = false
}
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
// 如果没有动态,则显示热门动态
timelineId = if (existMoments.list.isEmpty()) null else AppState.UserId,
trend = if (existMoments.list.isEmpty()) true else null
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it
}
}
}
fun updateLikeCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun likeMoment(id: Int) {
momentService.likeMoment(id)
updateLikeCount(id)
}
fun updateCommentCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(commentCount = momentItem.commentCount + 1)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun onAddComment(id: Int) {
val currentPagingData = _momentsFlow.value
updateCommentCount(id)
}
fun updateDislikeMomentById(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun dislikeMoment(id: Int) {
momentService.dislikeMoment(id)
updateDislikeMomentById(id)
}
fun updateFavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
updateFavoriteCount(id)
}
fun updateUnfavoriteCount(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id)
updateUnfavoriteCount(id)
}
fun deleteMoment(id: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.filter { momentItem ->
momentItem.id != id
}
_momentsFlow.value = updatedPagingData
}
/**
* 更新动态评论数
*/
fun updateMomentCommentCount(id: Int, delta: Int) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
momentItem.copy(commentCount = momentItem.commentCount + delta)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.authorId == authorId) {
momentItem.copy(followStatus = isFollow)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
}
fun followAction(moment: MomentEntity) {
viewModelScope.launch {
try {
if (moment.followStatus) {
userService.unFollowUser(moment.authorId.toString())
} else {
userService.followUser(moment.authorId.toString())
}
updateFollowStatus(moment.authorId, !moment.followStatus)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun ResetModel() {
_momentsFlow.value = PagingData.empty()
isFirstLoad = true
}
}

View File

@@ -0,0 +1,17 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
import org.greenrobot.eventbus.EventBus
object TimelineMomentViewModel : BaseMomentModel() {
init {
EventBus.getDefault().register(this)
}
override fun extraArgs(): MomentLoaderExtraArgs {
return MomentLoaderExtraArgs(timelineId = AppState.UserId!!)
}
}

View File

@@ -8,11 +8,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.filter
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.Messaging import com.aiosman.ravenow.Messaging
@@ -22,28 +17,40 @@ import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource import com.aiosman.ravenow.entity.MomentLoader
import com.aiosman.ravenow.entity.MomentRemoteDataSource import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import kotlinx.coroutines.flow.MutableStateFlow import com.aiosman.ravenow.event.MomentAddEvent
import kotlinx.coroutines.flow.asStateFlow import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import kotlinx.coroutines.flow.collectLatest import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.io.File import java.io.File
object MyProfileViewModel : ViewModel() { object MyProfileViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
val momentService: MomentService = MomentServiceImpl() val momentService: MomentService = MomentServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
private var _sharedFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty()) var moments by mutableStateOf<List<MomentEntity>>(emptyList())
var sharedFlow = _sharedFlow.asStateFlow() val momentLoader: MomentLoader = MomentLoader().apply {
onListChanged = {
moments = it
}
}
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var firstLoad = true var firstLoad = true
init {
EventBus.getDefault().register(this)
}
suspend fun loadUserProfile() { suspend fun loadUserProfile() {
val profile = accountService.getMyAccountProfile() val profile = accountService.getMyAccountProfile()
MyProfileViewModel.profile = profile MyProfileViewModel.profile = profile
} }
fun loadProfile(pullRefresh: Boolean = false) { fun loadProfile(pullRefresh: Boolean = false) {
if (!firstLoad && !pullRefresh) return if (!firstLoad && !pullRefresh) return
viewModelScope.launch { viewModelScope.launch {
@@ -55,18 +62,7 @@ object MyProfileViewModel : ViewModel() {
refreshing = false refreshing = false
profile?.let { profile?.let {
try { try {
// Collect shared flow momentLoader.loadData(extra = MomentLoaderExtraArgs(authorId = it.id))
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
author = AppState.UserId
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_sharedFlow.value = it
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyProfileViewModel", "loadProfile: ", e) Log.e("MyProfileViewModel", "loadProfile: ", e)
} }
@@ -75,6 +71,12 @@ object MyProfileViewModel : ViewModel() {
} }
} }
fun loadMoreMoment() {
viewModelScope.launch {
momentLoader.loadMore(extra = MomentLoaderExtraArgs(authorId = profile?.id))
}
}
fun logout(context: Context) { fun logout(context: Context) {
viewModelScope.launch { viewModelScope.launch {
Messaging.unregisterDevice(context) Messaging.unregisterDevice(context)
@@ -117,13 +119,30 @@ object MyProfileViewModel : ViewModel() {
} }
} }
fun deleteMoment(id: Int) { fun likeMoment(momentLMomentEntity: MomentEntity) {
val currentPagingData = _sharedFlow.value viewModelScope.launch {
val updatedPagingData = currentPagingData.filter { momentItem -> if (momentLMomentEntity.liked) {
momentItem.id != id momentService.dislikeMoment(momentLMomentEntity.id)
EventBus.getDefault().post(
MomentLikeChangeEvent(
momentLMomentEntity.id,
likeCount = momentLMomentEntity.likeCount - 1,
isLike = false
)
)
} else {
momentService.likeMoment(momentLMomentEntity.id)
EventBus.getDefault().post(
MomentLikeChangeEvent(
momentLMomentEntity.id,
likeCount = momentLMomentEntity.likeCount + 1,
isLike = true
)
)
} }
_sharedFlow.value = updatedPagingData
} }
}
val bio get() = profile?.bio ?: "" val bio get() = profile?.bio ?: ""
val nickName get() = profile?.nickName ?: "" val nickName get() = profile?.nickName ?: ""
@@ -131,7 +150,38 @@ object MyProfileViewModel : ViewModel() {
fun ResetModel() { fun ResetModel() {
profile = null profile = null
_sharedFlow.value = PagingData.empty() momentLoader.clear()
firstLoad = true firstLoad = true
} }
override fun onCleared() {
super.onCleared()
EventBus.getDefault().unregister(this)
}
@Subscribe
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
momentLoader.updateMomentLike(event.postId, event.isLike)
}
@Subscribe
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
}
@Subscribe
fun onRemoveMomentEvent(event: MomentRemoveEvent) {
momentLoader.removeMoment(event.postId)
}
@Subscribe
fun onAddMomentEvent(event: MomentAddEvent) {
momentLoader.addMoment(event.moment)
}
@Subscribe
fun onMomentDelete(event: MomentRemoveEvent) {
momentLoader.removeMoment(event.postId)
}
} }

View File

@@ -23,8 +23,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -38,6 +40,8 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -58,8 +62,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
@@ -88,9 +90,6 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.NewPostViewModel import com.aiosman.ravenow.ui.post.NewPostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@@ -102,22 +101,23 @@ fun ProfileV3(
onLogout: () -> Unit = {}, onLogout: () -> Unit = {},
onFollowClick: () -> Unit = {}, onFollowClick: () -> Unit = {},
onChatClick: () -> Unit = {}, onChatClick: () -> Unit = {},
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>( moments: List<MomentEntity>,
PagingData.empty() isSelf: Boolean = true,
).asStateFlow(), onLoadMore: () -> Unit = {},
isSelf: Boolean = true onLike: (MomentEntity) -> Unit = {},
onComment: (MomentEntity) -> Unit = {},
) { ) {
val model = MyProfileViewModel val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState() val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { 2 }) val pagerState = rememberPagerState(pageCount = { 2 })
var enabled by remember { mutableStateOf(true) } val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var minibarExpanded by remember { mutableStateOf(false) } var minibarExpanded by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
var bannerHeight = 400 val bannerHeight = 400
val pickBannerImageLauncher = pickupAndCompressLauncher( val pickBannerImageLauncher = pickupAndCompressLauncher(
context, context,
scope, scope,
@@ -126,15 +126,44 @@ fun ProfileV3(
) { uri, file -> ) { uri, file ->
onUpdateBanner?.invoke(uri, file, context) onUpdateBanner?.invoke(uri, file, context)
} }
val moments = sharedFlow.collectAsLazyPagingItems()
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = { val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
model.loadProfile(pullRefresh = true) model.loadProfile(pullRefresh = true)
}) })
var miniToolbarHeight by remember { mutableStateOf(0) } var miniToolbarHeight by remember { mutableStateOf(0) }
val density = LocalDensity.current val density = LocalDensity.current
var appTheme = LocalAppTheme.current val appTheme = LocalAppTheme.current
var AppColors = appTheme val AppColors = appTheme
var systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val listState = rememberLazyListState()
// observe list scrolling
val reachedListBottom by remember {
derivedStateOf {
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 2
}
}
// load more if scrolled to bottom
LaunchedEffect(reachedListBottom) {
if (reachedListBottom) {
onLoadMore()
}
}
val gridState = rememberLazyStaggeredGridState()
val reachedGridBottom by remember {
derivedStateOf {
val lastVisibleItem = gridState.layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index != 0 && lastVisibleItem?.index == gridState.layoutInfo.totalItemsCount - 2
}
}
LaunchedEffect(reachedGridBottom) {
if (reachedGridBottom) {
onLoadMore()
}
}
fun switchTheme(){ fun switchTheme(){
// delay // delay
scope.launch { scope.launch {
@@ -499,7 +528,8 @@ fun ProfileV3(
8.dp 8.dp
), ),
verticalItemSpacing = 8.dp, verticalItemSpacing = 8.dp,
contentPadding = PaddingValues(8.dp) contentPadding = PaddingValues(8.dp),
state = gridState
) { ) {
if (isSelf) { if (isSelf) {
items(1) { items(1) {
@@ -541,7 +571,7 @@ fun ProfileV3(
} }
} }
} }
items(moments.itemCount) { idx -> items(moments.size) { idx ->
val moment = moments[idx] ?: return@items val moment = moments[idx] ?: return@items
GalleryItem(moment, idx) GalleryItem(moment, idx)
} }
@@ -553,18 +583,26 @@ fun ProfileV3(
1 -> 1 ->
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize(),
state = listState
) { ) {
if (moments.itemCount == 0 && isSelf) { if (moments.isEmpty() && isSelf) {
item { item {
EmptyMomentPostUnit() EmptyMomentPostUnit()
} }
} }
item { item {
for (idx in 0 until moments.itemCount) { for (element in moments) {
val moment = moments[idx] element.let {
moment?.let { MomentPostUnit(
MomentPostUnit(it) it,
onLikeClick = {
onLike(it)
},
onCommentClick = {
onComment(it)
}
)
} }
} }
} }

View File

@@ -3,12 +3,15 @@ package com.aiosman.ravenow.ui.index.tabs.profile
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.ui.navigateToPost
@Composable @Composable
fun ProfileWrap( fun ProfileWrap(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
MyProfileViewModel.loadProfile() MyProfileViewModel.loadProfile()
} }
@@ -20,6 +23,15 @@ fun ProfileWrap(
MyProfileViewModel.logout(context) MyProfileViewModel.logout(context)
}, },
profile = MyProfileViewModel.profile, profile = MyProfileViewModel.profile,
sharedFlow = MyProfileViewModel.sharedFlow, moments = MyProfileViewModel.moments,
onLoadMore = {
MyProfileViewModel.loadMoreMoment()
},
onLike = { moments ->
MyProfileViewModel.likeMoment(moments)
},
onComment = {
navController.navigateToPost(it.id)
}
) )
} }

View File

@@ -47,7 +47,9 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.formatPostTime2 import com.aiosman.ravenow.exp.formatPostTime2
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.AnimatedLikeIcon
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.MomentOperateBtn
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.post.NewPostViewModel import com.aiosman.ravenow.ui.post.NewPostViewModel
@@ -130,20 +132,26 @@ fun ProfileEmptyMomentCard(
} }
@Composable @Composable
fun MomentPostUnit(momentEntity: MomentEntity) { fun MomentPostUnit(
momentEntity: MomentEntity,
onCommentClick: () -> Unit = {},
onLikeClick: () -> Unit = {}
) {
TimeGroup(momentEntity.time.formatPostTime2()) TimeGroup(momentEntity.time.formatPostTime2())
ProfileMomentCard( ProfileMomentCard(
momentEntity.momentTextContent, momentEntity.momentTextContent,
momentEntity.images[0].thumbnail, momentEntity.images[0].thumbnail,
momentEntity.likeCount.toString(), momentEntity.likeCount.toString(),
momentEntity.commentCount.toString(), momentEntity.commentCount.toString(),
momentEntity = momentEntity momentEntity = momentEntity,
onCommentClick = onCommentClick,
onLikeClick = onLikeClick
) )
} }
@Composable @Composable
fun TimeGroup(time: String = "2024.06.08 12:23") { fun TimeGroup(time: String = "2024.06.08 12:23") {
val AppColors = LocalAppTheme.current val appColors = LocalAppTheme.current
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -162,7 +170,7 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
Text( Text(
text = time, text = time,
fontSize = 16.sp, fontSize = 16.sp,
color = AppColors.text, color = appColors.text,
style = TextStyle(fontWeight = FontWeight.W600) style = TextStyle(fontWeight = FontWeight.W600)
) )
} }
@@ -174,7 +182,9 @@ fun ProfileMomentCard(
imageUrl: String, imageUrl: String,
like: String, like: String,
comment: String, comment: String,
momentEntity: MomentEntity momentEntity: MomentEntity,
onCommentClick: () -> Unit = {},
onLikeClick: () -> Unit = {}
) { ) {
var columnHeight by remember { mutableStateOf(0) } var columnHeight by remember { mutableStateOf(0) }
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -214,7 +224,13 @@ fun ProfileMomentCard(
MomentCardTopContent(content) MomentCardTopContent(content)
} }
MomentCardPicture(imageUrl, momentEntity = momentEntity) MomentCardPicture(imageUrl, momentEntity = momentEntity)
MomentCardOperation(like, comment) MomentCardOperation(
like = like,
isLike = momentEntity.liked,
onLikeClick = onLikeClick,
comment = comment,
onCommentClick = onCommentClick
)
} }
} }
} }
@@ -262,7 +278,13 @@ fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
} }
@Composable @Composable
fun MomentCardOperation(like: String, comment: String) { fun MomentCardOperation(
like: String,
isLike: Boolean,
onLikeClick: () -> Unit,
comment: String,
onCommentClick: () -> Unit
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -270,18 +292,26 @@ fun MomentCardOperation(like: String, comment: String) {
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentCardOperationItem( MomentOperateBtn(count = like) {
drawable = R.drawable.rider_pro_like, AnimatedLikeIcon(
number = like, liked = isLike,
modifier = Modifier.padding(end = 32.dp) onClick = onLikeClick,
) )
MomentCardOperationItem( }
drawable = R.drawable.rider_pro_moment_comment,
number = comment, MomentOperateBtn(count = comment) {
modifier = Modifier.padding(end = 32.dp) Image(
painter = painterResource(id = R.drawable.rider_pro_moment_comment),
contentDescription = "",
colorFilter = ColorFilter.tint(LocalAppTheme.current.text),
modifier = Modifier.noRippleClickable {
onCommentClick()
}
) )
} }
} }
}
@Composable @Composable
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) { fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {

View File

@@ -200,7 +200,7 @@ class CommentsViewModel(
replyUserId = replyUserId, replyUserId = replyUserId,
replyCommentId = replyCommentId replyCommentId = replyCommentId
) )
TimelineMomentViewModel.updateCommentCount(postId.toInt()) // TimelineMomentViewModel.updateCommentCount(postId.toInt())
// add to first // add to first
addedCommentList = listOf(comment) + addedCommentList addedCommentList = listOf(comment) + addedCommentList
} }

View File

@@ -14,6 +14,7 @@ import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.exp.rotate import com.aiosman.ravenow.exp.rotate
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
@@ -21,6 +22,7 @@ import com.aiosman.ravenow.ui.modification.Modification
import com.aiosman.ravenow.utils.FileUtil import com.aiosman.ravenow.utils.FileUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@@ -141,10 +143,10 @@ object NewPostViewModel : ViewModel() {
onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50% onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50%
index += 1 index += 1
} }
momentService.createMoment(textContent, 1, uploadImageList, relPostId) val result = momentService.createMoment(textContent, 1, uploadImageList, relPostId)
// 刷新个人动态 // 刷新个人动态
MyProfileViewModel.loadProfile(pullRefresh = true) MyProfileViewModel.loadProfile(pullRefresh = true)
TimelineMomentViewModel.refreshPager() EventBus.getDefault().post(MomentAddEvent(result))
} }
suspend fun init() { suspend fun init() {

View File

@@ -13,9 +13,14 @@ import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
class PostViewModel( class PostViewModel(
@@ -31,6 +36,9 @@ class PostViewModel(
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId) var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
var isError by mutableStateOf(false) var isError by mutableStateOf(false)
var isFirstLoad by mutableStateOf(true) var isFirstLoad by mutableStateOf(true)
init {
EventBus.getDefault().register(this)
}
fun reloadComment() { fun reloadComment() {
commentsViewModel.reloadComment() commentsViewModel.reloadComment()
@@ -77,37 +85,80 @@ class PostViewModel(
moment = moment?.copy(commentCount = moment?.commentCount?.plus(1) ?: 0) moment = moment?.copy(commentCount = moment?.commentCount?.plus(1) ?: 0)
} }
@Subscribe
fun onMomentLikeChangeEvent(event: MomentLikeChangeEvent) {
moment?.let {
if (event.postId == it.id) {
moment = it.copy(likeCount = event.likeCount ?: it.likeCount, liked = event.isLike)
}
}
}
suspend fun likeMoment() { suspend fun likeMoment() {
moment?.let { moment?.let {
service.likeMoment(it.id) service.likeMoment(it.id)
moment = moment?.copy(likeCount = moment?.likeCount?.plus(1) ?: 0, liked = true) EventBus.getDefault().post(
TimelineMomentViewModel.updateLikeCount(it.id) MomentLikeChangeEvent(
postId = it.id,
likeCount = it.likeCount + 1,
isLike = true
)
)
} }
} }
suspend fun dislikeMoment() { suspend fun dislikeMoment() {
moment?.let { moment?.let {
service.dislikeMoment(it.id) service.dislikeMoment(it.id)
moment = moment?.copy(likeCount = moment?.likeCount?.minus(1) ?: 0, liked = false) EventBus.getDefault().post(
// update home list MomentLikeChangeEvent(
TimelineMomentViewModel.updateDislikeMomentById(it.id) postId = it.id,
likeCount = it.likeCount - 1,
isLike = false
)
)
}
}
@Subscribe
fun onMomentFavouriteChangeEvent(event: MomentFavouriteChangeEvent) {
moment?.let {
if (event.postId == it.id) {
val favouriteCount = if (event.isFavourite) {
it.favoriteCount + 1
} else {
it.favoriteCount - 1
}
moment = it.copy(
favoriteCount = favouriteCount,
isFavorite = event.isFavourite
)
}
} }
} }
suspend fun favoriteMoment() { suspend fun favoriteMoment() {
moment?.let { moment?.let {
service.favoriteMoment(it.id) service.favoriteMoment(it.id)
moment = EventBus.getDefault().post(
moment?.copy(favoriteCount = moment?.favoriteCount?.plus(1) ?: 0, isFavorite = true) MomentFavouriteChangeEvent(
postId = it.id,
isFavourite = true
)
)
} }
} }
suspend fun unfavoriteMoment() { suspend fun unfavoriteMoment() {
moment?.let { moment?.let {
service.unfavoriteMoment(it.id) service.unfavoriteMoment(it.id)
moment = moment?.copy( EventBus.getDefault().post(
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, isFavorite = false MomentFavouriteChangeEvent(
postId = it.id,
isFavourite = false
) )
)
} }
} }
@@ -131,7 +182,6 @@ class PostViewModel(
commentsViewModel.deleteComment(commentId) commentsViewModel.deleteComment(commentId)
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0) moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
moment?.let { moment?.let {
TimelineMomentViewModel.updateMomentCommentCount(it.id, -1)
} }
} }
@@ -160,8 +210,7 @@ class PostViewModel(
viewModelScope.launch { viewModelScope.launch {
moment?.let { moment?.let {
service.deleteMoment(it.id) service.deleteMoment(it.id)
TimelineMomentViewModel.deleteMoment(it.id) EventBus.getDefault().post(MomentRemoveEvent(it.id))
MyProfileViewModel.deleteMoment(it.id)
} }
callback() callback()
} }
@@ -171,4 +220,8 @@ class PostViewModel(
commentsViewModel.loadMoreSubComments(commentId) commentsViewModel.loadMoreSubComments(commentId)
} }
override fun onCleared() {
super.onCleared()
EventBus.getDefault().unregister(this)
}
} }

View File

@@ -24,7 +24,7 @@ fun AccountProfileV2(id: String){
isSelf = true isSelf = true
} }
ProfileV3( ProfileV3(
sharedFlow = model.momentsFlow, moments = model.moments,
profile = model.profile, profile = model.profile,
isSelf = isSelf, isSelf = isSelf,
onChatClick = { onChatClick = {

View File

@@ -5,34 +5,29 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aiosman.ravenow.data.AccountService import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource import com.aiosman.ravenow.entity.MomentLoader
import com.aiosman.ravenow.entity.MomentRemoteDataSource import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.entity.MomentServiceImpl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AccountProfileViewModel : ViewModel() { class AccountProfileViewModel : ViewModel() {
var profileId by mutableStateOf(0) var profileId by mutableStateOf(0)
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
val momentService: MomentService = MomentServiceImpl()
val userService = UserServiceImpl() val userService = UserServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
var momentsFlow = _momentsFlow.asStateFlow()
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var momentLoader = MomentLoader().apply {
onListChanged = {
moments = it
}
}
var moments by mutableStateOf<List<MomentEntity>>(listOf())
fun loadProfile(id: String, pullRefresh: Boolean = false) { fun loadProfile(id: String, pullRefresh: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
if (pullRefresh) { if (pullRefresh) {
@@ -43,16 +38,12 @@ class AccountProfileViewModel : ViewModel() {
} }
profile = userService.getUserProfile(id) profile = userService.getUserProfile(id)
refreshing = false refreshing = false
Pager( profile?.let {
config = PagingConfig(pageSize = 5, enablePlaceholders = false), try {
pagingSourceFactory = { momentLoader.loadData(MomentLoaderExtraArgs(authorId = it.id))
MomentPagingSource( } catch (e: Exception) {
MomentRemoteDataSource(momentService), e.printStackTrace()
author = profile!!.id
)
} }
).flow.cachedIn(viewModelScope).collectLatest {
_momentsFlow.value = it
} }
} }
} }
@@ -72,8 +63,6 @@ class AccountProfileViewModel : ViewModel() {
} }
} }
val followerCount get() = profile?.followerCount ?: 0
val followingCount get() = profile?.followingCount ?: 0
val bio get() = profile?.bio ?: "" val bio get() = profile?.bio ?: ""
val nickName get() = profile?.nickName ?: "" val nickName get() = profile?.nickName ?: ""
val avatar get() = profile?.avatar val avatar get() = profile?.avatar