改包名com.aiosman.ravenow
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
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.data.CommentRemoteDataSource
|
||||
import com.aiosman.ravenow.data.CommentService
|
||||
import com.aiosman.ravenow.data.CommentServiceImpl
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.entity.CommentPagingSource
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CommentsViewModel(
|
||||
var postId: String = 0.toString(),
|
||||
) : ViewModel() {
|
||||
var commentService: CommentService = CommentServiceImpl()
|
||||
private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
||||
val commentsFlow = _commentsFlow.asStateFlow()
|
||||
var order: String by mutableStateOf("like")
|
||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
||||
var highlightCommentId by mutableStateOf<Int?>(null)
|
||||
var highlightComment by mutableStateOf<CommentEntity?>(null)
|
||||
|
||||
/**
|
||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||
*/
|
||||
fun preTransit() {
|
||||
viewModelScope.launch {
|
||||
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt()
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载评论
|
||||
*/
|
||||
fun reloadComment() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Pager(config = PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt(),
|
||||
order = order
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun highlightComment(commentId: Int) {
|
||||
highlightCommentId = commentId
|
||||
val resp = commentService.getCommentById(commentId)
|
||||
highlightComment = resp
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新高亮评论点赞状态
|
||||
*/
|
||||
private fun updateHighlightCommentLike(commentId: Int, isLike: Boolean): Boolean {
|
||||
var isUpdate = false
|
||||
highlightComment?.let {
|
||||
if (it.id == commentId) {
|
||||
highlightComment =
|
||||
it.copy(liked = isLike, likes = if (isLike) it.likes + 1 else it.likes - 1)
|
||||
isUpdate = true
|
||||
}
|
||||
highlightComment = it.copy(
|
||||
reply = it.reply.map { replyComment ->
|
||||
if (replyComment.id == commentId) {
|
||||
isUpdate = true
|
||||
replyComment.copy(
|
||||
liked = isLike,
|
||||
likes = if (isLike) replyComment.likes + 1 else replyComment.likes - 1
|
||||
)
|
||||
} else {
|
||||
replyComment
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return isUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新添加的评论点赞状态
|
||||
*/
|
||||
private fun updateAddedCommentLike(commentId: Int, isLike: Boolean): Boolean {
|
||||
var isUpdate = false
|
||||
addedCommentList = addedCommentList.map {
|
||||
if (it.id == commentId) {
|
||||
isUpdate = true
|
||||
it.copy(liked = isLike, likes = if (isLike) it.likes + 1 else it.likes - 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
return isUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评论点赞状态
|
||||
*/
|
||||
private fun updateCommentLike(commentId: Int, isLike: Boolean) {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { comment ->
|
||||
if (comment.id == commentId) {
|
||||
comment.copy(
|
||||
liked = isLike,
|
||||
likes = if (isLike) comment.likes + 1 else comment.likes - 1
|
||||
)
|
||||
} else {
|
||||
// 可能是回复的评论
|
||||
comment.copy(reply = comment.reply.map { replyComment ->
|
||||
if (replyComment.id == commentId) {
|
||||
replyComment.copy(
|
||||
liked = isLike,
|
||||
likes = if (isLike) replyComment.likes + 1 else replyComment.likes - 1
|
||||
)
|
||||
} else {
|
||||
replyComment
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
_commentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞评论
|
||||
*/
|
||||
suspend fun likeComment(commentId: Int) {
|
||||
try {
|
||||
commentService.likeComment(commentId)
|
||||
// 更新addCommentList
|
||||
if (updateHighlightCommentLike(commentId, true)) {
|
||||
return
|
||||
}
|
||||
if (updateAddedCommentLike(commentId, true)) {
|
||||
return
|
||||
}
|
||||
updateCommentLike(commentId, true)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消点赞评论
|
||||
suspend fun unlikeComment(commentId: Int) {
|
||||
commentService.dislikeComment(commentId)
|
||||
// 更新高亮评论点赞状态
|
||||
if (updateHighlightCommentLike(commentId, false)) {
|
||||
return
|
||||
}
|
||||
// 更新添加的评论点赞状态
|
||||
if (updateAddedCommentLike(commentId, false)) {
|
||||
return
|
||||
}
|
||||
// 更新评论点赞状态
|
||||
updateCommentLike(commentId, false)
|
||||
}
|
||||
|
||||
suspend fun createComment(
|
||||
content: String,
|
||||
parentCommentId: Int? = null,
|
||||
replyUserId: Int? = null,
|
||||
replyCommentId: Int? = null
|
||||
) {
|
||||
val comment =
|
||||
commentService.createComment(
|
||||
postId = postId.toInt(),
|
||||
content = content,
|
||||
parentCommentId = parentCommentId,
|
||||
replyUserId = replyUserId,
|
||||
replyCommentId = replyCommentId
|
||||
)
|
||||
TimelineMomentViewModel.updateCommentCount(postId.toInt())
|
||||
// add to first
|
||||
addedCommentList = listOf(comment) + addedCommentList
|
||||
}
|
||||
|
||||
fun deleteComment(commentId: Int) {
|
||||
viewModelScope.launch {
|
||||
commentService.DeleteComment(commentId)
|
||||
// 如果是刚刚创建的评论,则从addedCommentList中删除
|
||||
if (addedCommentList.any { it.id == commentId }) {
|
||||
addedCommentList = addedCommentList.filter { it.id != commentId }
|
||||
} else {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.filter { it.id != commentId }
|
||||
_commentsFlow.value = updatedPagingData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreSubComments(commentId: Int) {
|
||||
if (highlightComment?.id == commentId) {
|
||||
// 高亮的评论,更新高亮评论的回复
|
||||
highlightComment?.let {
|
||||
viewModelScope.launch {
|
||||
val subCommentList = commentService.getComments(
|
||||
postId = postId.toInt(),
|
||||
parentCommentId = commentId,
|
||||
pageNumber = it.replyPage + 1,
|
||||
pageSize = 3,
|
||||
).list
|
||||
highlightComment = it.copy(
|
||||
reply = it.reply.plus(subCommentList),
|
||||
replyPage = it.replyPage + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 普通评论
|
||||
viewModelScope.launch {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { comment ->
|
||||
if (comment.id == commentId) {
|
||||
try {
|
||||
subCommentLoadingMap[commentId] = true
|
||||
val subCommentList = commentService.getComments(
|
||||
postId = postId.toInt(),
|
||||
parentCommentId = commentId,
|
||||
pageNumber = comment.replyPage + 1,
|
||||
pageSize = 3,
|
||||
order = "earliest"
|
||||
).list
|
||||
return@map comment.copy(
|
||||
reply = comment.reply.plus(subCommentList),
|
||||
replyPage = comment.replyPage + 1
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@map comment.copy()
|
||||
} finally {
|
||||
subCommentLoadingMap[commentId] = false
|
||||
}
|
||||
}
|
||||
comment
|
||||
}
|
||||
_commentsFlow.value = updatedPagingData
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
544
app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt
Normal file
544
app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt
Normal file
@@ -0,0 +1,544 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.DraggableGrid
|
||||
import com.aiosman.ravenow.ui.composables.RelPostCard
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun NewPostScreen() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
val model = NewPostViewModel
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setNavigationBarColor(color = Color.Transparent)
|
||||
model.init()
|
||||
}
|
||||
|
||||
|
||||
StatusBarMaskLayout(
|
||||
darkIcons = !AppState.darkMode,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
AppColors.background
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
NewPostTopBar {
|
||||
}
|
||||
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
|
||||
NewPostViewModel.textContent = it
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
model.relMoment?.let {
|
||||
Text("Share with")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color = Color(0xFFEEEEEE))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
RelPostCard(
|
||||
momentEntity = it,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddImageGrid()
|
||||
// AdditionalPostItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
var uploading by remember { mutableStateOf(false) }
|
||||
// 上传进度
|
||||
if (uploading) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { },
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(64.dp)).shadow(elevation = 4.dp)
|
||||
.background(AppColors.background).padding(16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = AppColors.main
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Uploading", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = AppColors.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val model = NewPostViewModel
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp, vertical = 10.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
navController.popBackStack()
|
||||
},
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_video_share),
|
||||
tint = AppColors.text,
|
||||
contentDescription = "Send",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.noRippleClickable {
|
||||
// 检查输入
|
||||
val errorMessage = model.validateMoment()
|
||||
if (errorMessage != null) {
|
||||
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
|
||||
return@noRippleClickable
|
||||
}
|
||||
model.viewModelScope.launch {
|
||||
try {
|
||||
uploading = true
|
||||
model.createMoment(context = context) { progress ->
|
||||
// 更新进度条
|
||||
}
|
||||
navController.popBackStack()
|
||||
}catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
}finally {
|
||||
uploading = false
|
||||
}
|
||||
|
||||
// 上传完成后隐藏进度条
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(200.dp)
|
||||
.padding(horizontal = 18.dp, vertical = 10.dp),
|
||||
cursorBrush = SolidColor(AppColors.text),
|
||||
textStyle = TextStyle(
|
||||
color = AppColors.text,
|
||||
)
|
||||
|
||||
)
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = hint,
|
||||
color = AppColors.inputHint,
|
||||
modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AddImageGrid() {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val model = NewPostViewModel
|
||||
val scope = model.viewModelScope
|
||||
val pickImagesLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetMultipleContents()
|
||||
) { uris ->
|
||||
if (uris.isNotEmpty()) {
|
||||
scope.launch {
|
||||
for (uri in uris) {
|
||||
ImageItem.fromUri(context, uri.toString())?.let {
|
||||
model.imageList += it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val takePictureLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.TakePicture()
|
||||
) { success ->
|
||||
if (success) {
|
||||
scope.launch {
|
||||
ImageItem.fromUri(context, model.currentPhotoUri.toString())?.let {
|
||||
model.imageList += it
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val stroke = Stroke(
|
||||
width = 2f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
)
|
||||
DraggableGrid(
|
||||
items = NewPostViewModel.imageList,
|
||||
onMove = { from, to ->
|
||||
NewPostViewModel.imageList = NewPostViewModel.imageList.toMutableList().apply {
|
||||
add(to, removeAt(from))
|
||||
}
|
||||
},
|
||||
lockedIndices = listOf(
|
||||
|
||||
),
|
||||
onDragModeEnd = {},
|
||||
onDragModeStart = {},
|
||||
additionalItems = listOf(
|
||||
|
||||
),
|
||||
getItemId = { it.id }
|
||||
) { item, isDrag ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
LocalContext.current,
|
||||
item.bitmap,
|
||||
contentDescription = "Image",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.NewPostImageGrid.route)
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
if (isDrag) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x66000000))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(18.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.drawBehind {
|
||||
val strokeWidth = 1.dp.toPx()
|
||||
val dashLength = 10f
|
||||
val dashGap = 10f
|
||||
val pathEffect =
|
||||
PathEffect.dashPathEffect(floatArrayOf(dashLength, dashGap))
|
||||
drawRoundRect(
|
||||
color = Color(0xFFD6D6D6),
|
||||
style = Stroke(strokeWidth, pathEffect = pathEffect),
|
||||
cornerRadius = CornerRadius(8.dp.toPx())
|
||||
)
|
||||
}
|
||||
.noRippleClickable {
|
||||
pickImagesLauncher.launch("image/*")
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
||||
contentDescription = "Add Image",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.Center),
|
||||
tint = Color(0xFFD6D6D6)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.drawBehind {
|
||||
val strokeWidth = 1.dp.toPx()
|
||||
val dashLength = 10f
|
||||
val dashGap = 10f
|
||||
val pathEffect =
|
||||
PathEffect.dashPathEffect(floatArrayOf(dashLength, dashGap))
|
||||
drawRoundRect(
|
||||
color = Color(0xFFD6D6D6),
|
||||
style = Stroke(strokeWidth, pathEffect = pathEffect),
|
||||
cornerRadius = CornerRadius(8.dp.toPx())
|
||||
)
|
||||
}
|
||||
.noRippleClickable {
|
||||
val photoFile = File(context.cacheDir, "photo.jpg")
|
||||
val photoUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
photoFile
|
||||
)
|
||||
model.currentPhotoUri = photoUri
|
||||
takePictureLauncher.launch(photoUri)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_camera),
|
||||
contentDescription = "Take Photo",
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.Center),
|
||||
tint = Color(0xFFD6D6D6)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AdditionalPostItem() {
|
||||
val model = NewPostViewModel
|
||||
val navController = LocalNavController.current
|
||||
var isShowLocationModal by remember { mutableStateOf(false) }
|
||||
fun onSelectLocationClick() {
|
||||
isShowLocationModal = true
|
||||
}
|
||||
if (isShowLocationModal) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
isShowLocationModal = false
|
||||
},
|
||||
containerColor = Color.White
|
||||
|
||||
) {
|
||||
// Sheet content
|
||||
SelectLocationModal(
|
||||
onClose = {
|
||||
isShowLocationModal = false
|
||||
}
|
||||
) {
|
||||
isShowLocationModal = false
|
||||
NewPostViewModel.searchPlaceAddressResult = it
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onSelectLocationClick()
|
||||
}
|
||||
) {
|
||||
NewPostViewModel.searchPlaceAddressResult?.let {
|
||||
SelectedLocation(it) {
|
||||
NewPostViewModel.searchPlaceAddressResult = null
|
||||
}
|
||||
} ?: Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_add_location),
|
||||
contentDescription = "Location",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text("Add Location", color = Color(0xFF333333))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_nav_next),
|
||||
contentDescription = "Add Location",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
navController.navigate("EditModification")
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_modification),
|
||||
contentDescription = "Modification List",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text("Modification List", color = Color(0xFF333333))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_nav_next),
|
||||
contentDescription = "Modification List",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedLocation(
|
||||
searchPlaceAddressResult: SearchPlaceAddressResult,
|
||||
onRemoveLocation: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp)
|
||||
) {
|
||||
Text(searchPlaceAddressResult.name, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(searchPlaceAddressResult.address, color = Color(0xFF9a9a9a))
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_close),
|
||||
contentDescription = "Next",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onRemoveLocation()
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NewPostImageGridScreen() {
|
||||
val model = NewPostViewModel
|
||||
val imageList = model.imageList
|
||||
val pagerState = rememberPagerState(pageCount = { imageList.size })
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
val navController = LocalNavController.current
|
||||
val title = "${pagerState.currentPage + 1}/${imageList.size}"
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF2e2e2e))
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = "back",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
navController.popBackStack()
|
||||
},
|
||||
tint = Color.White
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
color = Color.White,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "delete",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
model.deleteImage(pagerState.currentPage)
|
||||
},
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
) { page ->
|
||||
val imageUrl = imageList[page]
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(model = imageUrl.bitmap),
|
||||
contentDescription = "Image $page",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.ExifInterface
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import com.aiosman.ravenow.data.UploadImage
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.exp.rotate
|
||||
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.modification.Modification
|
||||
import com.aiosman.ravenow.utils.FileUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
|
||||
data class ImageItem(
|
||||
val uri: String,
|
||||
val id: String,
|
||||
val bitmap: Bitmap,
|
||||
val file: File
|
||||
) {
|
||||
companion object {
|
||||
suspend fun fromUri(context: Context,uri: String): ImageItem? {
|
||||
// 保存图片文件到临时文件夹
|
||||
context.contentResolver.openInputStream(Uri.parse(uri))?.use { inputStream ->
|
||||
val tempFileName = UUID.randomUUID().toString()
|
||||
val tempFile = File.createTempFile(tempFileName, null, context.cacheDir)
|
||||
FileOutputStream(tempFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
|
||||
// 读取图片文件为 Bitmap
|
||||
var bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
|
||||
// 读取文件exif,修正旋转
|
||||
val exif = ExifInterface(tempFile.absolutePath)
|
||||
bitmap = when (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> bitmap.rotate(90)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> bitmap.rotate(180)
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> bitmap.rotate(270)
|
||||
else -> bitmap
|
||||
}
|
||||
// 保存 bitmap 到临时文件夹
|
||||
try {
|
||||
val savedBitmapFilename = UUID.randomUUID().toString()
|
||||
val bitmapFile = File.createTempFile(savedBitmapFilename, ".jpg", context.cacheDir)
|
||||
FileOutputStream(bitmapFile).use { os ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
|
||||
}
|
||||
// 清理临时文件
|
||||
tempFile.delete()
|
||||
return ImageItem(
|
||||
Uri.fromFile(bitmapFile).toString(),
|
||||
savedBitmapFilename,
|
||||
bitmap,
|
||||
bitmapFile
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e("NewPost", "Failed to save bitmap to file", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NewPostViewModel : ViewModel() {
|
||||
var momentService: MomentService = MomentServiceImpl()
|
||||
var textContent by mutableStateOf("")
|
||||
var searchPlaceAddressResult by mutableStateOf<SearchPlaceAddressResult?>(null)
|
||||
var modificationList by mutableStateOf<List<Modification>>(listOf())
|
||||
var imageList by mutableStateOf(listOf<ImageItem>())
|
||||
var relPostId by mutableStateOf<Int?>(null)
|
||||
var relMoment by mutableStateOf<MomentEntity?>(null)
|
||||
var currentPhotoUri: Uri? = null
|
||||
fun asNewPost() {
|
||||
textContent = ""
|
||||
searchPlaceAddressResult = null
|
||||
modificationList = listOf()
|
||||
imageList = listOf()
|
||||
relPostId = null
|
||||
}
|
||||
|
||||
fun asNewPostWithImageUris(imageUris: List<String>) {
|
||||
textContent = ""
|
||||
searchPlaceAddressResult = null
|
||||
modificationList = listOf()
|
||||
// imageList = imageUris.map {
|
||||
// ImageItem(it, UUID.randomUUID().toString())
|
||||
// }
|
||||
relPostId = null
|
||||
}
|
||||
|
||||
suspend fun uriToFile(context: Context, uri: Uri): File {
|
||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||
val tempFile = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("temp", null, context.cacheDir)
|
||||
}
|
||||
inputStream?.use { input ->
|
||||
FileOutputStream(tempFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return tempFile
|
||||
}
|
||||
|
||||
fun validateMoment(): String? {
|
||||
if (imageList.isEmpty()) {
|
||||
return "Please select at least one image"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun createMoment(context: Context, onUploadProgress: (Float) -> Unit) {
|
||||
val uploadImageList = emptyList<UploadImage>().toMutableList()
|
||||
var index = 0
|
||||
for (item in imageList) {
|
||||
// 保存图片到本地
|
||||
FileUtil.bitmapToJPG(context, item.bitmap, UUID.randomUUID().toString())
|
||||
?.let { savedImageUri ->
|
||||
// 读取保存的图片文件
|
||||
uriToFile(context, savedImageUri).let { file ->
|
||||
uploadImageList += UploadImage(file, file.name, item.uri, "jpg")
|
||||
}
|
||||
}
|
||||
// 在上传过程中调用 onUploadProgress 更新进度
|
||||
onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50%
|
||||
index += 1
|
||||
}
|
||||
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
|
||||
// 刷新个人动态
|
||||
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||
TimelineMomentViewModel.refreshPager()
|
||||
}
|
||||
|
||||
suspend fun init() {
|
||||
relPostId?.let {
|
||||
val moment = momentService.getMomentById(it)
|
||||
relMoment = moment
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteImage(index: Int) {
|
||||
imageList = imageList.toMutableList().apply {
|
||||
removeAt(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
1600
app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt
Normal file
1600
app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt
Normal file
File diff suppressed because it is too large
Load Diff
174
app/src/main/java/com/aiosman/ravenow/ui/post/PostViewModel.kt
Normal file
174
app/src/main/java/com/aiosman/ravenow/ui/post/PostViewModel.kt
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
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.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.data.UserService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class PostViewModel(
|
||||
val postId: String
|
||||
) : ViewModel() {
|
||||
var service: MomentService = MomentServiceImpl()
|
||||
var userService: UserService = UserServiceImpl()
|
||||
|
||||
|
||||
var accountProfileEntity by mutableStateOf<AccountProfileEntity?>(null)
|
||||
var moment by mutableStateOf<MomentEntity?>(null)
|
||||
var accountService: AccountService = AccountServiceImpl()
|
||||
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId)
|
||||
var isError by mutableStateOf(false)
|
||||
var isFirstLoad by mutableStateOf(true)
|
||||
|
||||
fun reloadComment() {
|
||||
commentsViewModel.reloadComment()
|
||||
}
|
||||
|
||||
suspend fun initData(highlightCommentId: Int? = null) {
|
||||
if (!isFirstLoad) {
|
||||
return
|
||||
}
|
||||
isFirstLoad = false
|
||||
try {
|
||||
moment = service.getMomentById(postId.toInt())
|
||||
} catch (e: Exception) {
|
||||
isError = true
|
||||
return
|
||||
}
|
||||
highlightCommentId?.let {
|
||||
commentsViewModel.highlightComment(it)
|
||||
}
|
||||
commentsViewModel.reloadComment()
|
||||
|
||||
}
|
||||
|
||||
suspend fun likeComment(commentId: Int) {
|
||||
commentsViewModel.likeComment(commentId)
|
||||
}
|
||||
|
||||
suspend fun unlikeComment(commentId: Int) {
|
||||
commentsViewModel.unlikeComment(commentId)
|
||||
}
|
||||
|
||||
suspend fun createComment(
|
||||
content: String,
|
||||
parentCommentId: Int? = null,
|
||||
replyUserId: Int? = null,
|
||||
replyCommentId: Int? = null
|
||||
) {
|
||||
commentsViewModel.createComment(
|
||||
content = content,
|
||||
parentCommentId = parentCommentId,
|
||||
replyUserId = replyUserId,
|
||||
replyCommentId = replyCommentId
|
||||
)
|
||||
moment = moment?.copy(commentCount = moment?.commentCount?.plus(1) ?: 0)
|
||||
}
|
||||
|
||||
suspend fun likeMoment() {
|
||||
moment?.let {
|
||||
service.likeMoment(it.id)
|
||||
moment = moment?.copy(likeCount = moment?.likeCount?.plus(1) ?: 0, liked = true)
|
||||
TimelineMomentViewModel.updateLikeCount(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun dislikeMoment() {
|
||||
moment?.let {
|
||||
service.dislikeMoment(it.id)
|
||||
moment = moment?.copy(likeCount = moment?.likeCount?.minus(1) ?: 0, liked = false)
|
||||
// update home list
|
||||
TimelineMomentViewModel.updateDislikeMomentById(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun favoriteMoment() {
|
||||
moment?.let {
|
||||
service.favoriteMoment(it.id)
|
||||
moment =
|
||||
moment?.copy(favoriteCount = moment?.favoriteCount?.plus(1) ?: 0, isFavorite = true)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfavoriteMoment() {
|
||||
moment?.let {
|
||||
service.unfavoriteMoment(it.id)
|
||||
moment = moment?.copy(
|
||||
favoriteCount = moment?.favoriteCount?.minus(1) ?: 0, isFavorite = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun followUser() {
|
||||
moment?.let {
|
||||
userService.followUser(it.authorId.toString())
|
||||
moment = moment?.copy(followStatus = true)
|
||||
// 更新我的关注页面的关注数
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfollowUser() {
|
||||
moment?.let {
|
||||
userService.unFollowUser(it.authorId.toString())
|
||||
moment = moment?.copy(followStatus = false)
|
||||
// 更新我的关注页面的关注数
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteComment(commentId: Int) {
|
||||
commentsViewModel.deleteComment(commentId)
|
||||
moment = moment?.copy(commentCount = moment?.commentCount?.minus(1) ?: 0)
|
||||
moment?.let {
|
||||
TimelineMomentViewModel.updateMomentCommentCount(it.id, -1)
|
||||
}
|
||||
}
|
||||
|
||||
var avatar: String? = null
|
||||
get() {
|
||||
accountProfileEntity?.avatar?.let {
|
||||
return it
|
||||
}
|
||||
moment?.avatar?.let {
|
||||
return it
|
||||
}
|
||||
return field
|
||||
}
|
||||
var nickname: String? = null
|
||||
get() {
|
||||
accountProfileEntity?.nickName?.let {
|
||||
return it
|
||||
}
|
||||
moment?.nickname?.let {
|
||||
return it
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
fun deleteMoment(callback: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
moment?.let {
|
||||
service.deleteMoment(it.id)
|
||||
TimelineMomentViewModel.deleteMoment(it.id)
|
||||
MyProfileViewModel.deleteMoment(it.id)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreSubComments(commentId: Int) {
|
||||
commentsViewModel.loadMoreSubComments(commentId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.R
|
||||
|
||||
|
||||
data class SearchPlaceAddressResult(
|
||||
val name: String,
|
||||
val address: String
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SelectLocationModal(
|
||||
onClose: () -> Unit,
|
||||
onSelectedLocation: (SearchPlaceAddressResult) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var queryString by remember { mutableStateOf("") }
|
||||
var searchPlaceAddressResults by remember {
|
||||
mutableStateOf<List<SearchPlaceAddressResult>>(
|
||||
emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
// fun searchAddrWithGoogleMap(query: String) {
|
||||
// val placesClient: PlacesClient = Places.createClient(context)
|
||||
// val placeFields: List<Place.Field> =
|
||||
// listOf(Place.Field.ID, Place.Field.NAME, Place.Field.ADDRESS)
|
||||
// val request = SearchByTextRequest.newInstance(query, placeFields)
|
||||
// placesClient.searchByText(request)
|
||||
// .addOnSuccessListener { response ->
|
||||
// val place = response.places
|
||||
// searchPlaceAddressResults = place.map {
|
||||
// SearchPlaceAddressResult(it.name ?: "", it.address ?: "")
|
||||
// }
|
||||
//
|
||||
// }.addOnFailureListener { exception ->
|
||||
// if (exception is ApiException) {
|
||||
// Log.e("SelectLocationModal", "Place not found: ${exception.statusCode}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp)
|
||||
) {
|
||||
Text(
|
||||
"Check In",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
Text(
|
||||
"Cancel",
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.clickable {
|
||||
onClose()
|
||||
},
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
LocationSearchTextInput(queryString, onQueryClick = {
|
||||
// searchAddrWithGoogleMap(queryString)
|
||||
}) {
|
||||
queryString = it
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(top = 28.dp)
|
||||
) {
|
||||
item {
|
||||
for (searchPlaceAddressResult in searchPlaceAddressResults) {
|
||||
LocationItem(searchPlaceAddressResult) {
|
||||
onSelectedLocation(searchPlaceAddressResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocationSearchTextInput(
|
||||
value: String,
|
||||
onQueryClick: () -> Unit,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp)
|
||||
.clip(shape = RoundedCornerShape(16.dp))
|
||||
.background(Color(0xffF5F5F5))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_search_location),
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
"search",
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
color = Color(0xffA0A0A0)
|
||||
)
|
||||
}
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
onQueryClick()
|
||||
// hide keyboard
|
||||
keyboardController?.hide()
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocationItem(
|
||||
searchPlaceAddressResult: SearchPlaceAddressResult,
|
||||
onLocationItemClick: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
.clickable {
|
||||
onLocationItemClick()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp)
|
||||
) {
|
||||
Text(searchPlaceAddressResult.name, fontWeight = FontWeight.Bold)
|
||||
Text(searchPlaceAddressResult.address, color = Color(0xFF9a9a9a))
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_nav_next),
|
||||
contentDescription = "Next",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user