This commit introduces a new "Create Post" screen, enabling users to compose and publish posts with text and up to three images. The implementation includes a dedicated ViewModel, robust state management, and an asynchronous image upload flow.
**Key Changes:**
* **feat(Create Post Screen):**
* Added `CreatePostScreen.kt`, a new Composable screen for creating posts.
* The UI includes a text input field, selectors for "Section" and "Category," and an image picker/preview area.
* Implemented `SelectionBottomSheet` as a reusable component for picking sections and categories from a modal bottom sheet.
* The "Publish" button is dynamically enabled based on content validation and image upload status.
* Error messages are displayed to the user via a snackbar-like component.
* **feat(CreatePostViewModel):**
* Created a new `CreatePostViewModel` to manage the logic and state (`CreatePostState`) of the creation process.
* Fetches post categories on initialization.
* Handles image selection and manages an asynchronous upload queue. Each selected image is immediately uploaded to the backend via `MediaRepository`.
* The ViewModel tracks the upload progress for each image and updates the UI accordingly.
* The `createPost` function validates all inputs, compiles the post data (including uploaded image UUIDs), and sends it to the repository.
* **feat(Navigation):**
* Added a `CreatePost` route to the navigation graph (`Screen.kt`).
* Integrated the new screen into `App.kt` with custom horizontal slide transitions.
* The publish button on the main screen's top bar now navigates to the `CreatePostScreen`.
* Upon successful post creation, the user is navigated back to the main screen.
* **refactor(PostDetailViewModel):**
* Adjusted the `createComment` logic to correctly handle `parentCommentId`, sending `0L` instead of `null` when there is no parent comment.
329 lines
11 KiB
Kotlin
329 lines
11 KiB
Kotlin
package com.qingshuige.tangyuan.viewmodel
|
||
|
||
import android.content.Context
|
||
import androidx.lifecycle.ViewModel
|
||
import androidx.lifecycle.viewModelScope
|
||
import com.qingshuige.tangyuan.model.CommentCard
|
||
import com.qingshuige.tangyuan.model.CreateCommentDto
|
||
import com.qingshuige.tangyuan.model.PostCard
|
||
import com.qingshuige.tangyuan.model.PostDetailState
|
||
import com.qingshuige.tangyuan.network.TokenManager
|
||
import com.qingshuige.tangyuan.repository.PostDetailRepository
|
||
import com.qingshuige.tangyuan.utils.ImageSaveUtils
|
||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||
import kotlinx.coroutines.flow.MutableStateFlow
|
||
import kotlinx.coroutines.flow.StateFlow
|
||
import kotlinx.coroutines.flow.asStateFlow
|
||
import kotlinx.coroutines.flow.catch
|
||
import kotlinx.coroutines.launch
|
||
import java.util.Date
|
||
import javax.inject.Inject
|
||
|
||
@HiltViewModel
|
||
class PostDetailViewModel @Inject constructor(
|
||
private val postDetailRepository: PostDetailRepository,
|
||
@ApplicationContext private val context: Context
|
||
) : ViewModel() {
|
||
|
||
private val tokenManager = TokenManager()
|
||
|
||
private val _state = MutableStateFlow(PostDetailState())
|
||
val state: StateFlow<PostDetailState> = _state.asStateFlow()
|
||
|
||
private var currentPostId: Int = 0
|
||
private var currentUserId: Int = 0
|
||
|
||
/**
|
||
* 加载帖子详情和评论 - 分离加载,先加载帖子再加载评论
|
||
*/
|
||
fun loadPostDetail(postId: Int) {
|
||
currentPostId = postId
|
||
currentUserId = tokenManager.getUserIdFromToken() ?: 0
|
||
|
||
viewModelScope.launch {
|
||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||
|
||
try {
|
||
// 先加载帖子详情,立即更新UI
|
||
postDetailRepository.getPostCard(postId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
isLoading = false,
|
||
error = e.message ?: "加载帖子失败"
|
||
)
|
||
}
|
||
.collect { postCard ->
|
||
// 立即更新帖子数据,确保共享元素有目标
|
||
_state.value = _state.value.copy(
|
||
postCard = postCard,
|
||
isLoading = false
|
||
)
|
||
|
||
// 然后异步加载评论
|
||
loadComments(postId, currentUserId)
|
||
}
|
||
} catch (e: Exception) {
|
||
_state.value = _state.value.copy(
|
||
isLoading = false,
|
||
error = e.message ?: "加载失败"
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载评论数据
|
||
*/
|
||
private fun loadComments(postId: Int, userId: Int = 0) {
|
||
viewModelScope.launch {
|
||
_state.value = _state.value.copy(isLoading = true) // 这里的loading只影响评论区
|
||
|
||
postDetailRepository.getCommentCardsForPost(postId, userId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
isLoading = false,
|
||
error = e.message ?: "加载评论失败"
|
||
)
|
||
}
|
||
.collect { commentCards ->
|
||
_state.value = _state.value.copy(
|
||
isLoading = false,
|
||
comments = commentCards,
|
||
error = null
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新帖子详情
|
||
*/
|
||
fun refreshPostDetail() {
|
||
if (currentPostId == 0) return
|
||
|
||
viewModelScope.launch {
|
||
_state.value = _state.value.copy(isRefreshing = true)
|
||
|
||
postDetailRepository.refreshPostDetail(currentPostId, currentUserId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
isRefreshing = false,
|
||
error = e.message ?: "刷新失败"
|
||
)
|
||
}
|
||
.collect { (postCard, commentCards) ->
|
||
_state.value = _state.value.copy(
|
||
isRefreshing = false,
|
||
postCard = postCard,
|
||
comments = commentCards,
|
||
error = null
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发布新评论
|
||
*/
|
||
fun createComment(content: String, parentCommentId: Int = 0) {
|
||
if (currentPostId == 0 || content.isBlank()) return
|
||
|
||
viewModelScope.launch {
|
||
_state.value = _state.value.copy(
|
||
isCreatingComment = true,
|
||
commentError = null
|
||
)
|
||
|
||
val createCommentDto = CreateCommentDto(
|
||
postId = currentPostId.toLong(),
|
||
commentDateTime = Date(),
|
||
content = content,
|
||
parentCommentId = if (parentCommentId == 0) 0L else parentCommentId.toLong(),
|
||
userId = currentUserId.toLong()
|
||
)
|
||
|
||
postDetailRepository.createComment(createCommentDto)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
isCreatingComment = false,
|
||
commentError = e.message ?: "评论发布失败"
|
||
)
|
||
}
|
||
.collect { message ->
|
||
_state.value = _state.value.copy(
|
||
isCreatingComment = false,
|
||
replyToComment = null,
|
||
commentError = null
|
||
)
|
||
// 评论发布成功后,刷新评论列表
|
||
refreshComments()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除评论
|
||
*/
|
||
fun deleteComment(commentId: Int) {
|
||
viewModelScope.launch {
|
||
postDetailRepository.deleteComment(commentId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
error = e.message ?: "删除评论失败"
|
||
)
|
||
}
|
||
.collect { success ->
|
||
if (success) {
|
||
// 删除成功后,从本地列表中移除该评论
|
||
val updatedComments = _state.value.comments.filter { comment ->
|
||
comment.commentId != commentId &&
|
||
comment.replies.none { it.commentId == commentId }
|
||
}.map { comment ->
|
||
comment.copy(
|
||
replies = comment.replies.filter { it.commentId != commentId }
|
||
)
|
||
}
|
||
|
||
_state.value = _state.value.copy(comments = updatedComments)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置回复目标评论
|
||
*/
|
||
fun setReplyToComment(commentCard: CommentCard?) {
|
||
_state.value = _state.value.copy(replyToComment = commentCard)
|
||
}
|
||
|
||
/**
|
||
* 刷新评论列表
|
||
*/
|
||
private fun refreshComments() {
|
||
if (currentPostId == 0) return
|
||
|
||
viewModelScope.launch {
|
||
postDetailRepository.getCommentCardsForPost(currentPostId, currentUserId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
error = e.message ?: "刷新评论失败"
|
||
)
|
||
}
|
||
.collect { commentCards ->
|
||
_state.value = _state.value.copy(comments = commentCards)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载更多评论
|
||
*/
|
||
fun loadMoreComments() {
|
||
// TODO: 实现分页加载评论
|
||
// 当前实现一次加载所有评论,后续可以根据需要实现分页
|
||
}
|
||
|
||
/**
|
||
* 展开/收起评论回复
|
||
*/
|
||
fun toggleReplies(commentId: Int) {
|
||
viewModelScope.launch {
|
||
val updatedComments = _state.value.comments.map { comment ->
|
||
if (comment.commentId == commentId) {
|
||
if (comment.replies.isEmpty() && comment.replyCount > 0) {
|
||
// 加载回复
|
||
loadRepliesForComment(commentId)
|
||
comment
|
||
} else {
|
||
// 切换显示状态(这里简单处理,实际可能需要添加展开状态字段)
|
||
comment
|
||
}
|
||
} else {
|
||
comment
|
||
}
|
||
}
|
||
_state.value = _state.value.copy(comments = updatedComments)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载特定评论的回复
|
||
*/
|
||
private fun loadRepliesForComment(commentId: Int) {
|
||
viewModelScope.launch {
|
||
postDetailRepository.getReplyCardsForComment(commentId, currentUserId)
|
||
.catch { e ->
|
||
_state.value = _state.value.copy(
|
||
error = e.message ?: "加载回复失败"
|
||
)
|
||
}
|
||
.collect { replyCards ->
|
||
val updatedComments = _state.value.comments.map { comment ->
|
||
if (comment.commentId == commentId) {
|
||
comment.copy(replies = replyCards)
|
||
} else {
|
||
comment
|
||
}
|
||
}
|
||
_state.value = _state.value.copy(comments = updatedComments)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除错误状态
|
||
*/
|
||
fun clearError() {
|
||
_state.value = _state.value.copy(error = null)
|
||
}
|
||
|
||
/**
|
||
* 清除评论错误状态
|
||
*/
|
||
fun clearCommentError() {
|
||
_state.value = _state.value.copy(commentError = null)
|
||
}
|
||
|
||
/**
|
||
* 重置状态
|
||
*/
|
||
fun resetState() {
|
||
_state.value = PostDetailState()
|
||
currentPostId = 0
|
||
currentUserId = 0
|
||
}
|
||
|
||
/**
|
||
* 保存当前图片到本地
|
||
*/
|
||
fun saveCurrentImage(imageUrl: String) {
|
||
viewModelScope.launch {
|
||
try {
|
||
val result = ImageSaveUtils.saveImageToGallery(context, imageUrl)
|
||
result.onSuccess { message ->
|
||
_state.value = _state.value.copy(
|
||
error = null,
|
||
saveMessage = message
|
||
)
|
||
}.onFailure { exception ->
|
||
_state.value = _state.value.copy(
|
||
error = exception.message ?: "保存图片失败"
|
||
)
|
||
}
|
||
} catch (e: Exception) {
|
||
_state.value = _state.value.copy(
|
||
error = e.message ?: "保存图片失败"
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除保存消息
|
||
*/
|
||
fun clearSaveMessage() {
|
||
_state.value = _state.value.copy(saveMessage = null)
|
||
}
|
||
} |