diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c8cbf6..f567213 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -101,6 +101,5 @@ dependencies { implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-perf") implementation("com.google.firebase:firebase-messaging-ktx") - } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b62647..a76750c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ + - + + - + @@ -49,6 +52,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/MainActivity.kt b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt index b982779..c2af4b6 100644 --- a/app/src/main/java/com/aiosman/riderpro/MainActivity.kt +++ b/app/src/main/java/com/aiosman/riderpro/MainActivity.kt @@ -101,9 +101,15 @@ class MainActivity : ComponentActivity() { val postId = intent.getStringExtra("POST_ID") if (postId != null) { Log.d("MainActivity", "Navigation to Post$postId") - navController.navigate(NavigationRoute.Post.route.replace("{id}", postId)) + navController.navigate( + NavigationRoute.Post.route.replace( + "{id}", + postId + ) + ) } } + } } diff --git a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt index 8226e4a..fc9a8d2 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt @@ -460,11 +460,17 @@ class AccountServiceImpl : AccountService { } override suspend fun resetPassword(email: String) { - ApiClient.api.resetPassword( + val resp = ApiClient.api.resetPassword( ResetPasswordRequestBody( username = email ) ) + if (!resp.isSuccessful) { + parseErrorResponse(resp.errorBody())?.let { + throw it.toServiceException() + } + throw ServiceException("Failed to reset password") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt index b8076e3..cf290b9 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt @@ -155,7 +155,8 @@ class CommentRemoteDataSource( postUser: Int?, selfNotice: Boolean?, order: String?, - parentCommentId: Int? + parentCommentId: Int?, + pageSize: Int? = 20 ): ListContainer { return commentService.getComments( pageNumber, @@ -163,7 +164,8 @@ class CommentRemoteDataSource( postUser = postUser, selfNotice = selfNotice, order = order, - parentCommentId = parentCommentId + parentCommentId = parentCommentId, + pageSize = pageSize ) } } diff --git a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt index fea49c0..627aeef 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/MomentService.kt @@ -29,7 +29,9 @@ data class Moment( @SerializedName("commentCount") val commentCount: Long, @SerializedName("time") - val time: String + val time: String, + @SerializedName("isFollowed") + val isFollowed: Boolean, ) { fun toMomentItem(): MomentEntity { return MomentEntity( @@ -38,7 +40,7 @@ data class Moment( nickname = user.nickName, location = "Worldwide", time = ApiClient.dateFromApiString(time), - followStatus = false, + followStatus = isFollowed, momentTextContent = textContent, momentPicture = R.drawable.default_moment_img, likeCount = likeCount.toInt(), diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt index 73a737c..f734c0e 100644 --- a/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt +++ b/app/src/main/java/com/aiosman/riderpro/entity/Comment.kt @@ -46,14 +46,15 @@ class CommentPagingSource( postUser = postUser, selfNotice = selfNotice, order = order, - parentCommentId = parentCommentId + parentCommentId = parentCommentId, + pageSize = params.loadSize ) LoadResult.Page( data = comments.list, prevKey = if (currentPage == 1) null else currentPage - 1, nextKey = if (comments.list.isEmpty()) null else comments.page + 1 ) - } catch (exception: IOException) { + } catch (exception: Exception) { return LoadResult.Error(exception) } } diff --git a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt index 5979544..04a8a28 100644 --- a/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt +++ b/app/src/main/java/com/aiosman/riderpro/entity/Moment.kt @@ -8,6 +8,7 @@ import com.aiosman.riderpro.data.MomentService import com.aiosman.riderpro.data.ServiceException import com.aiosman.riderpro.data.UploadImage import com.aiosman.riderpro.data.api.ApiClient +import com.aiosman.riderpro.data.parseErrorResponse import com.aiosman.riderpro.entity.MomentEntity import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -165,8 +166,13 @@ class MomentBackend { suspend fun getMomentById(id: Int): MomentEntity { var resp = ApiClient.api.getPost(id) - var body = resp.body()?.data ?: throw ServiceException("Failed to get moment") - return body.toMomentItem() + if (!resp.isSuccessful) { + parseErrorResponse(resp.errorBody())?.let { + throw it.toServiceException() + } + throw ServiceException("Failed to get moment") + } + return resp.body()?.data?.toMomentItem() ?: throw ServiceException("Failed to get moment") } suspend fun likeMoment(id: Int) { diff --git a/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt b/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt index 82eef66..94086fa 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/account/ResetPassword.kt @@ -49,8 +49,17 @@ fun ResetPasswordScreen() { var isSendSuccess by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(false) } val navController = LocalNavController.current - + var usernameError by remember { mutableStateOf(null) } + fun validate(): Boolean { + if (username.isEmpty()) { + usernameError = context.getString(R.string.text_error_email_required) + return false + } + usernameError = null + return true + } fun resetPassword() { + if (!validate()) return scope.launch { isLoading = true try { @@ -78,7 +87,7 @@ fun ResetPasswordScreen() { ) ) { NoticeScreenHeader( - "RECOVER ACCOUNT", + stringResource(R.string.recover_account_upper), moreIcon = false ) } @@ -93,7 +102,7 @@ fun ResetPasswordScreen() { if (isSendSuccess!!) { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Reset password email has been sent to your email address", + text = stringResource(R.string.reset_mail_send_success), style = TextStyle( color = Color(0xFF333333), fontSize = 14.sp, @@ -103,7 +112,7 @@ fun ResetPasswordScreen() { } else { Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Failed to send reset password email", + text = stringResource(R.string.reset_mail_send_failed), style = TextStyle( color = Color(0xFF333333), fontSize = 14.sp, @@ -138,7 +147,8 @@ fun ResetPasswordScreen() { onValueChange = { username = it }, label = stringResource(R.string.login_email_label), hint = stringResource(R.string.text_hint_email), - enabled = !isLoading + enabled = !isLoading, + error = usernameError ) Spacer(modifier = Modifier.height(72.dp)) if (isLoading) { @@ -148,7 +158,7 @@ fun ResetPasswordScreen() { modifier = Modifier .width(345.dp) .height(48.dp), - text = "Recover Account", + text = stringResource(R.string.recover), backgroundImage = R.mipmap.rider_pro_signup_red_bg ) { resetPassword() diff --git a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt index e2ebbbf..b277865 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/comment/CommentModal.kt @@ -1,6 +1,5 @@ package com.aiosman.riderpro.ui.comment -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,11 +13,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars 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.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ModalBottomSheet @@ -30,17 +26,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope 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.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -48,15 +40,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.compose.collectAsLazyPagingItems -import com.aiosman.riderpro.AppState import com.aiosman.riderpro.R import com.aiosman.riderpro.entity.CommentEntity import com.aiosman.riderpro.ui.composables.EditCommentBottomModal -import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.post.CommentContent import com.aiosman.riderpro.ui.post.CommentMenuModal -import com.aiosman.riderpro.ui.post.CommentsSection import com.aiosman.riderpro.ui.post.CommentsViewModel import com.aiosman.riderpro.ui.post.OrderSelectionComponent import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt new file mode 100644 index 0000000..b0c0d47 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/CustomClickableText.kt @@ -0,0 +1,50 @@ +package com.aiosman.riderpro.ui.composables + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow + +@Composable +fun CustomClickableText( + text: AnnotatedString, + modifier: Modifier = Modifier, + style: TextStyle = TextStyle.Default, + softWrap: Boolean = true, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + onLongPress: () -> Unit = {}, + onClick: (Int) -> Unit +) { + val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = Modifier.pointerInput(onClick) { + detectTapGestures( + onLongPress = { onLongPress() } + ) { pos -> + layoutResult.value?.let { layoutResult -> + onClick(layoutResult.getOffsetForPosition(pos)) + } + } + } + + BasicText( + text = text, + modifier = modifier.then(pressIndicator), + style = style, + softWrap = softWrap, + overflow = overflow, + maxLines = maxLines, + onTextLayout = { + layoutResult.value = it + onTextLayout(it) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt new file mode 100644 index 0000000..9551ccf --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/DragAndDrop.kt @@ -0,0 +1,278 @@ +package com.aiosman.riderpro.ui.composables + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.zIndex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DraggableGrid( + items: List, + onMove: (Int, Int) -> Unit, + onDragModeStart: () -> Unit, // New parameter for drag start + onDragModeEnd: () -> Unit, // New parameter for drag end, + additionalItems: List<@Composable () -> Unit> = emptyList(), // New parameter for additional items + lockedIndices: List = emptyList(), // New parameter for locked indices + content: @Composable (T, Boolean) -> Unit, +) { + + val gridState = rememberLazyGridState() + val dragDropState = + rememberGridDragDropState(gridState, onMove, onDragModeStart, onDragModeEnd, lockedIndices) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier.dragContainer(dragDropState), + state = gridState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + + ) { + itemsIndexed(items, key = { _, item -> item }) { index, item -> + DraggableItem(dragDropState, index) { isDragging -> + content(item, isDragging) + } + } + additionalItems.forEach { additionalItem -> + item { + additionalItem() + } + } + + } +} + +fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier { + return pointerInput(dragDropState) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> dragDropState.onDragStart(offset) }, + onDragEnd = { dragDropState.onDragInterrupted() }, + onDragCancel = { dragDropState.onDragInterrupted() } + ) + } +} + +@ExperimentalFoundationApi +@Composable +fun LazyGridItemScope.DraggableItem( + dragDropState: GridDragDropState, + index: Int, + modifier: Modifier = Modifier, + content: @Composable (isDragging: Boolean) -> Unit, +) { + val dragging = index == dragDropState.draggingItemIndex + val draggingModifier = if (dragging) { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = dragDropState.draggingItemOffset.x + translationY = dragDropState.draggingItemOffset.y + } + } else if (index == dragDropState.previousIndexOfDraggedItem) { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = dragDropState.previousItemOffset.value.x + translationY = dragDropState.previousItemOffset.value.y + } + } else { + Modifier.animateItemPlacement() + } + Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) { + content(dragging) + } +} + + +@Composable +fun rememberGridDragDropState( + gridState: LazyGridState, + onMove: (Int, Int) -> Unit, + onDragModeStart: () -> Unit, + onDragModeEnd: () -> Unit, + lockedIndices: List // New parameter for locked indices +): GridDragDropState { + val scope = rememberCoroutineScope() + val state = remember(gridState) { + GridDragDropState( + state = gridState, + onMove = onMove, + scope = scope, + onDragModeStart = onDragModeStart, + onDragModeEnd = onDragModeEnd, + lockedIndices = lockedIndices // Pass the locked indices + ) + } + LaunchedEffect(state) { + while (true) { + val diff = state.scrollChannel.receive() + gridState.scrollBy(diff) + } + } + return state +} + +class GridDragDropState internal constructor( + private val state: LazyGridState, + private val scope: CoroutineScope, + private val onMove: (Int, Int) -> Unit, + private val onDragModeStart: () -> Unit, + private val onDragModeEnd: () -> Unit, + private val lockedIndices: List // New parameter for locked indices +) { + var draggingItemIndex by mutableStateOf(null) + private set + + internal val scrollChannel = Channel() + + private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero) + private var draggingItemInitialOffset by mutableStateOf(Offset.Zero) + internal val draggingItemOffset: Offset + get() = draggingItemLayoutInfo?.let { item -> + draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() + } ?: Offset.Zero + + private val draggingItemLayoutInfo: LazyGridItemInfo? + get() = state.layoutInfo.visibleItemsInfo + .firstOrNull { it.index == draggingItemIndex } + + internal var previousIndexOfDraggedItem by mutableStateOf(null) + private set + internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter) + private set + + internal fun onDragStart(offset: Offset) { + state.layoutInfo.visibleItemsInfo + .firstOrNull { item -> + offset.x.toInt() in item.offset.x..item.offsetEnd.x && + offset.y.toInt() in item.offset.y..item.offsetEnd.y + }?.also { + if (it.index !in lockedIndices) { // Check if the item is not locked + draggingItemIndex = it.index + draggingItemInitialOffset = it.offset.toOffset() + onDragModeStart() // Notify drag start + } + } + } + + internal fun onDragInterrupted() { + if (draggingItemIndex != null) { + previousIndexOfDraggedItem = draggingItemIndex + val startOffset = draggingItemOffset + scope.launch { + previousItemOffset.snapTo(startOffset) + previousItemOffset.animateTo( + Offset.Zero, + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = Offset.VisibilityThreshold + ) + ) + previousIndexOfDraggedItem = null + } + } + draggingItemDraggedDelta = Offset.Zero + draggingItemIndex = null + draggingItemInitialOffset = Offset.Zero + onDragModeEnd() // Notify drag end + } + + internal fun onDrag(offset: Offset) { + draggingItemDraggedDelta += offset + + val draggingItem = draggingItemLayoutInfo ?: return + val startOffset = draggingItem.offset.toOffset() + draggingItemOffset + val endOffset = startOffset + draggingItem.size.toSize() + val middleOffset = startOffset + (endOffset - startOffset) / 2f + + val targetItem = state.layoutInfo.visibleItemsInfo.find { item -> + middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x && + middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y && + draggingItem.index != item.index && + item.index !in lockedIndices // Check if the target item is not locked + } + if (targetItem != null) { + val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { + draggingItem.index + } else if (draggingItem.index == state.firstVisibleItemIndex) { + targetItem.index + } else { + null + } + if (scrollToIndex != null) { + scope.launch { + state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) + onMove.invoke(draggingItem.index, targetItem.index) + } + } else { + onMove.invoke(draggingItem.index, targetItem.index) + } + draggingItemIndex = targetItem.index + } else { + val overscroll = when { + draggingItemDraggedDelta.y > 0 -> + (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + + draggingItemDraggedDelta.y < 0 -> + (startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + + else -> 0f + } + if (overscroll != 0f) { + scrollChannel.trySend(overscroll) + } + } + } + + private val LazyGridItemInfo.offsetEnd: IntOffset + get() = this.offset + this.size +} + +operator fun IntOffset.plus(size: IntSize): IntOffset { + return IntOffset(x + size.width, y + size.height) +} + +operator fun Offset.plus(size: Size): Offset { + return Offset(x + size.width, y + size.height) +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt index f4a6bbc..3fd49c8 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/EditCommentBottomModal.kt @@ -1,5 +1,7 @@ package com.aiosman.riderpro.ui.composables +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -24,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -74,16 +77,25 @@ fun EditCommentBottomModal( fontSize = 20.sp, fontStyle = FontStyle.Italic ) - Image( - painter = painterResource(id = R.drawable.rider_pro_send), - contentDescription = "Send", - modifier = Modifier - .size(32.dp) - .noRippleClickable { - onSend(text) - text = "" - }, - ) + Crossfade(targetState = text.isNotEmpty(), animationSpec = tween(500)) { isNotEmpty -> + Image( + painter = rememberUpdatedState( + if (isNotEmpty) painterResource(id = R.drawable.rider_pro_send) else painterResource( + id = R.drawable.rider_pro_send_disable + ) + ).value, + contentDescription = "Send", + modifier = Modifier + .size(32.dp) + .noRippleClickable { + if (text.isNotEmpty()){ + onSend(text) + text = "" + } + + }, + ) + } } Spacer(modifier = Modifier.height(16.dp)) if (replyComment != null) { diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt new file mode 100644 index 0000000..53db789 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/FollowButton.kt @@ -0,0 +1,57 @@ +package com.aiosman.riderpro.ui.composables + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.aiosman.riderpro.R +import com.aiosman.riderpro.ui.modifiers.noRippleClickable + +@Composable +fun FollowButton( + isFollowing: Boolean, + fontSize: TextUnit = 12.sp, + imageModifier: Modifier = Modifier, + onFollowClick: () -> Unit, +){ + Box( + modifier = Modifier + .wrapContentWidth() + .padding(start = 6.dp) + .noRippleClickable { + onFollowClick() + }, + contentAlignment = Alignment.Center + ) { + Image( + modifier = imageModifier, + painter = painterResource(id = R.drawable.follow_bg), + contentDescription = "", + contentScale = ContentScale.FillWidth + ) + Text( + text = if (isFollowing) stringResource(R.string.following_upper) else stringResource( + R.string.follow_upper + ), + fontSize = fontSize, + color = Color.White, + style = TextStyle(fontWeight = FontWeight.Bold) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt index de547dc..16cb1b8 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/TextInputField.kt @@ -1,5 +1,9 @@ package com.aiosman.riderpro.ui.composables +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -112,16 +116,23 @@ fun TextInputField( .height(16.dp), verticalAlignment = Alignment.CenterVertically ) { - if (error != null) { - Image( - painter = painterResource(id = R.mipmap.rider_pro_input_error), - contentDescription = "Error", - modifier = Modifier.size(8.dp) - ) - Spacer(modifier = Modifier.size(4.dp)) - Text(error, color = Color(0xFFE53935), fontSize = 12.sp) + AnimatedVisibility( + visible = error != null, + enter = fadeIn(), + exit = fadeOut() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.mipmap.rider_pro_input_error), + contentDescription = "Error", + modifier = Modifier.size(8.dp) + ) + Spacer(modifier = Modifier.size(4.dp)) + AnimatedContent(targetState = error) { targetError -> + Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp) + } + } } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt index e7d1b2a..511d38b 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteListPage.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.LocalNavController @@ -47,7 +48,8 @@ fun FavouriteListPage() { Box( modifier = Modifier .fillMaxWidth() - .weight(1f).pullRefresh(state) + .weight(1f) + .pullRefresh(state) ) { Column( modifier = Modifier.fillMaxSize() @@ -57,7 +59,7 @@ fun FavouriteListPage() { .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp), ) { - NoticeScreenHeader("Favourite") + NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false) } LazyVerticalGrid( columns = GridCells.Fixed(3), diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt index 16af645..af8367e 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeScreen.kt @@ -30,6 +30,7 @@ fun FavouriteNoticeScreen() { var dataFlow = model.favouriteItemsFlow var favourites = dataFlow.collectAsLazyPagingItems() LaunchedEffect(Unit) { + model.reload() model.updateNotice() } StatusBarMaskLayout( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt index 436e3df..92adfbc 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteNoticeViewModel.kt @@ -26,8 +26,13 @@ object FavouriteNoticeViewModel : ViewModel() { private val _favouriteItemsFlow = MutableStateFlow>(PagingData.empty()) val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow() + var isFirstLoad = true - init { + fun reload(force: Boolean = false) { + if (!isFirstLoad && !force) { + return + } + isFirstLoad = false viewModelScope.launch { Pager( config = PagingConfig(pageSize = 5, enablePlaceholders = false), diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt index 7efa5c1..c2ed905 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerList.kt @@ -47,7 +47,11 @@ fun FollowerListScreen(userId: Int) { isFollowing = user.isFollowing ) { scope.launch { - model.followUser(user.id) + if (user.isFollowing) { + model.unFollowUser(user.id) + } else { + model.followUser(user.id) + } } } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt index 46f9a0a..78aab56 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerListViewModel.kt @@ -43,11 +43,11 @@ object FollowerListViewModel : ViewModel() { } } - private fun updateIsFollow(id: Int) { + private fun updateIsFollow(id: Int, isFollow: Boolean = true) { val currentPagingData = usersFlow.value val updatedPagingData = currentPagingData.map { user -> if (user.id == id) { - user.copy(isFollowing = true) + user.copy(isFollowing = isFollow) } else { user } @@ -60,4 +60,9 @@ object FollowerListViewModel : ViewModel() { updateIsFollow(userId) } + suspend fun unFollowUser(userId: Int) { + userService.unFollowUser(userId.toString()) + updateIsFollow(userId, false) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt index 988f1a5..dc433cb 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNotice.kt @@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.paging.compose.collectAsLazyPagingItems +import com.aiosman.riderpro.AppState import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.composables.FollowButton import com.aiosman.riderpro.ui.modifiers.noRippleClickable import kotlinx.coroutines.launch @@ -47,11 +49,14 @@ fun FollowerNoticeScreen() { var dataFlow = model.followerItemsFlow var followers = dataFlow.collectAsLazyPagingItems() Box( - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) ) { NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false) } LaunchedEffect(Unit) { + model.reload() model.updateNotice() } LazyColumn( @@ -114,30 +119,43 @@ fun FollowItem( ) { Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp) } - if (!isFollowing) { - Box( - modifier = Modifier.noRippleClickable { - onFollow() - } - ) { - Image( - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "Follow", - modifier = Modifier - .width(79.dp) - .height(24.dp) - ) - Text( - "FOLLOW", - fontSize = 14.sp, - color = Color(0xFFFFFFFF), - modifier = Modifier.align( - Alignment.Center - ) - ) - } + if (userId != AppState.UserId) { + FollowButton( + isFollowing = isFollowing, + onFollowClick = onFollow, + fontSize = 14.sp, + imageModifier = Modifier + .width(100.dp) + .height(24.dp) + ) } +// Box( +// modifier = Modifier.noRippleClickable { +// onFollow() +// } +// ) { +// Image( +// painter = painterResource(id = R.drawable.follow_bg), +// contentDescription = "Follow", +// modifier = Modifier +// .width(79.dp) +// .height(24.dp) +// ) +// Text( +// text = if (isFollowing) { +// stringResource(R.string.following_upper) +// } else { +// stringResource(R.string.follow_upper) +// }, +// fontSize = 14.sp, +// color = Color(0xFFFFFFFF), +// modifier = Modifier.align( +// Alignment.Center +// ) +// ) +// } + } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt index ef252e9..cb72ba3 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerNoticeViewModel.kt @@ -30,8 +30,13 @@ object FollowerNoticeViewModel : ViewModel() { private val _followerItemsFlow = MutableStateFlow>(PagingData.empty()) val followerItemsFlow = _followerItemsFlow.asStateFlow() + var isFirstLoad = true - init { + fun reload(force: Boolean = false) { + if (!isFirstLoad && !force) { + return + } + isFirstLoad = false viewModelScope.launch { Pager( config = PagingConfig(pageSize = 5, enablePlaceholders = false), diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt index e5ce050..201170c 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingList.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.launch @Composable fun FollowingListScreen(userId: Int) { - val model = FollowerListViewModel + val model = FollowingListViewModel val scope = rememberCoroutineScope() LaunchedEffect(Unit) { model.loadData(userId) @@ -47,7 +47,11 @@ fun FollowingListScreen(userId: Int) { isFollowing = user.isFollowing ) { scope.launch { - model.followUser(user.id) + if (user.isFollowing) { + model.unfollowUser(user.id) + } else { + model.followUser(user.id) + } } } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt index 6cc2f8e..fa25a5e 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowingListViewModel.kt @@ -24,9 +24,6 @@ object FollowingListViewModel : ViewModel() { val usersFlow = _usersFlow.asStateFlow() private var userId by mutableStateOf(null) fun loadData(id: Int) { - if (userId == id) { - return - } userId = id viewModelScope.launch { Pager( @@ -34,7 +31,7 @@ object FollowingListViewModel : ViewModel() { pagingSourceFactory = { AccountPagingSource( userService, - followerId = id + followingId = id ) } ).flow.cachedIn(viewModelScope).collectLatest { @@ -43,11 +40,11 @@ object FollowingListViewModel : ViewModel() { } } - private fun updateIsFollow(id: Int) { + private fun updateIsFollow(id: Int, isFollow: Boolean = true) { val currentPagingData = usersFlow.value val updatedPagingData = currentPagingData.map { user -> if (user.id == id) { - user.copy(isFollowing = true) + user.copy(isFollowing = isFollow) } else { user } @@ -60,4 +57,9 @@ object FollowingListViewModel : ViewModel() { updateIsFollow(userId) } + suspend fun unfollowUser(userId: Int) { + userService.unFollowUser(userId.toString()) + updateIsFollow(userId, false) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt index 06a7f66..07e168f 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/imageviewer/imageviewer.kt @@ -20,11 +20,8 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -35,30 +32,28 @@ import androidx.compose.runtime.rememberCoroutineScope 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.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.aiosman.riderpro.LocalAnimatedContentScope import com.aiosman.riderpro.LocalNavController -import com.aiosman.riderpro.LocalSharedTransitionScope import com.aiosman.riderpro.R import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.utils.File.saveImageToGallery -import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.launch import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable -@OptIn(ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class, - ExperimentalMaterial3Api::class +@OptIn( + ExperimentalFoundationApi::class, ) @Composable fun ImageViewer() { @@ -72,8 +67,11 @@ fun ImageViewer() { WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp val scope = rememberCoroutineScope() val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) } - var showBottomSheet by remember { mutableStateOf(false) } var isDownloading by remember { mutableStateOf(false) } + var currentPage by remember { mutableStateOf(model.initialIndex) } + LaunchedEffect(pagerState) { + currentPage = pagerState.currentPage + } StatusBarMaskLayout( modifier = Modifier.background(Color.Black), ) { @@ -84,7 +82,7 @@ fun ImageViewer() { ) { HorizontalPager( state = pagerState, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { page -> val zoomState = rememberZoomState() CustomAsyncImage( @@ -102,6 +100,23 @@ fun ImageViewer() { contentScale = ContentScale.Fit, ) } + if (images.size > 1) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0xff333333).copy(alpha = 0.6f)) + .padding(vertical = 4.dp, horizontal = 24.dp) + ) { + Text( + text = "${pagerState.currentPage + 1}/${images.size}", + color = Color.White, + + ) + } + } + + Box( modifier = Modifier @@ -142,7 +157,7 @@ fun ImageViewer() { modifier = Modifier.size(32.dp), color = Color.White ) - }else{ + } else { Icon( painter = painterResource(id = R.drawable.rider_pro_download), contentDescription = "", @@ -153,7 +168,7 @@ fun ImageViewer() { Spacer(modifier = Modifier.height(4.dp)) Text( - "Download", + stringResource(R.string.download), color = Color.White ) } @@ -174,7 +189,7 @@ fun ImageViewer() { ) Spacer(modifier = Modifier.height(4.dp)) Text( - "Original", + stringResource(R.string.original), color = Color.White ) } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt index a221b94..43aaec1 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageList.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -43,6 +44,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewModelScope +import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R @@ -52,6 +54,9 @@ import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.StatusBarSpacer +import com.aiosman.riderpro.ui.favourite.FavouriteNoticeViewModel +import com.aiosman.riderpro.ui.follower.FollowerNoticeViewModel +import com.aiosman.riderpro.ui.like.LikeNoticeViewModel import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.post.PostViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -70,7 +75,7 @@ fun NotificationsScreen() { var comments = dataFlow.collectAsLazyPagingItems() val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { MessageListViewModel.viewModelScope.launch { - MessageListViewModel.initData() + MessageListViewModel.initData(force = true) } }) LaunchedEffect(Unit) { @@ -81,10 +86,12 @@ fun NotificationsScreen() { modifier = Modifier.fillMaxSize() ) { StatusBarSpacer() - Box(modifier = Modifier - .fillMaxWidth() - .weight(1f) - .pullRefresh(state)) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .pullRefresh(state) + ) { Column( modifier = Modifier.fillMaxSize(), ) { @@ -99,6 +106,13 @@ fun NotificationsScreen() { R.drawable.rider_pro_like, stringResource(R.string.like_upper) ) { + if (MessageListViewModel.likeNoticeCount > 0) { + // 刷新点赞消息列表 + LikeNoticeViewModel.isFirstLoad = true + // 清除点赞消息数量 + MessageListViewModel.clearLikeNoticeCount() + } + navController.navigate(NavigationRoute.Likes.route) } NotificationIndicator( @@ -106,6 +120,11 @@ fun NotificationsScreen() { R.drawable.rider_pro_followers, stringResource(R.string.followers_upper) ) { + if (MessageListViewModel.followNoticeCount > 0) { + // 刷新关注消息列表 + FollowerNoticeViewModel.isFirstLoad = true + MessageListViewModel.clearFollowNoticeCount() + } navController.navigate(NavigationRoute.Followers.route) } NotificationIndicator( @@ -113,36 +132,101 @@ fun NotificationsScreen() { R.drawable.rider_pro_favoriate, stringResource(R.string.favourites_upper) ) { + if (MessageListViewModel.favouriteNoticeCount > 0) { + // 刷新收藏消息列表 + FavouriteNoticeViewModel.isFirstLoad = true + MessageListViewModel.clearFavouriteNoticeCount() + } navController.navigate(NavigationRoute.FavouritesScreen.route) } } HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp)) NotificationCounterItem(MessageListViewModel.commentNoticeCount) - LazyColumn( - modifier = Modifier - .weight(1f) - .fillMaxSize() - ) { - items(comments.itemCount) { index -> - comments[index]?.let { comment -> - CommentNoticeItem(comment) { - MessageListViewModel.updateReadStatus(comment.id) - MessageListViewModel.viewModelScope.launch { -// PostViewModel.postId = comment.postId.toString() -// PostViewModel.initData() - navController.navigate( - NavigationRoute.Post.route.replace( - "{id}", - comment.postId.toString() - ) - ) - } + if (comments.loadState.refresh is LoadState.Loading) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(bottom = 48.dp) + , + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Loading", + fontSize = 18.sp + ) + Spacer(modifier = Modifier.height(16.dp)) + LinearProgressIndicator( + modifier = Modifier.width(160.dp), + color = Color(0xFFDA3832) + ) + } + } + } else { + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxSize() + ) { + items(comments.itemCount) { index -> + comments[index]?.let { comment -> + CommentNoticeItem(comment) { + MessageListViewModel.updateReadStatus(comment.id) + MessageListViewModel.viewModelScope.launch { + navController.navigate( + NavigationRoute.Post.route.replace( + "{id}", + comment.postId.toString() + ) + ) + } + } } } - } - item { - Spacer(modifier = Modifier.height(72.dp)) + // handle load error + when { + comments.loadState.append is LoadState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + modifier = Modifier.width(160.dp), + color = Color(0xFFDA3832) + ) + } + } + } + + comments.loadState.append is LoadState.Error -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .noRippleClickable { + comments.retry() + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "Load comment error, click to retry", + ) + } + } + } + } + item { + Spacer(modifier = Modifier.height(72.dp)) + } } } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt index c442e21..bd9fee6 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/message/MessageListViewModel.kt @@ -31,9 +31,15 @@ object MessageListViewModel : ViewModel() { private val _commentItemsFlow = MutableStateFlow>(PagingData.empty()) val commentItemsFlow = _commentItemsFlow.asStateFlow() var isLoading by mutableStateOf(false) - - suspend fun initData() { - isLoading = true + var isFirstLoad = true + suspend fun initData(force: Boolean = false) { + if (!isFirstLoad && !force) { + return + } + if (force) { + isLoading = true + } + isFirstLoad = false val info = accountService.getMyNoticeInfo() noticeInfo = info viewModelScope.launch { @@ -43,7 +49,7 @@ object MessageListViewModel : ViewModel() { CommentPagingSource( CommentRemoteDataSource(commentService), selfNotice = true, - order="latest" + order = "latest" ) } ).flow.cachedIn(viewModelScope).collectLatest { @@ -51,6 +57,7 @@ object MessageListViewModel : ViewModel() { } } isLoading = false + } val likeNoticeCount @@ -80,4 +87,15 @@ object MessageListViewModel : ViewModel() { updateIsRead(id) } } + + fun clearLikeNoticeCount() { + noticeInfo = noticeInfo?.copy(likeCount = 0) + } + + fun clearFollowNoticeCount() { + noticeInfo = noticeInfo?.copy(followCount = 0) + } + fun clearFavouriteNoticeCount() { + noticeInfo = noticeInfo?.copy(favoriteCount = 0) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt index de97358..1d379d6 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt @@ -37,26 +37,36 @@ object MyProfileViewModel : ViewModel() { private var _momentsFlow = MutableStateFlow>(PagingData.empty()) var momentsFlow = _momentsFlow.asStateFlow() var refreshing by mutableStateOf(false) + var firstLoad = true fun loadProfile(pullRefresh: Boolean = false) { + if (!firstLoad && !pullRefresh) { + return + } viewModelScope.launch { if (pullRefresh){ refreshing = true } + firstLoad = false profile = accountService.getMyAccountProfile() val profile = accountService.getMyAccountProfile() refreshing = false - Pager( - config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - MomentPagingSource( - MomentRemoteDataSource(momentService), - author = profile.id - ) + try { + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + MomentPagingSource( + MomentRemoteDataSource(momentService), + author = profile.id + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _momentsFlow.value = it } - ).flow.cachedIn(viewModelScope).collectLatest { - _momentsFlow.value = it + }catch (e: Exception){ + Log.e("MyProfileViewModel", "loadProfile: ", e) } + } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt index 50750de..7b482a3 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profile.kt @@ -162,8 +162,6 @@ fun ProfilePage() { ) } } - - Box( modifier = Modifier .align(Alignment.TopEnd) @@ -174,10 +172,14 @@ fun ProfilePage() { ) ) { Box( - modifier = Modifier.padding(16.dp).clip(RoundedCornerShape(8.dp)).shadow( - elevation = 20.dp - ).background(Color.White.copy(alpha = 0.7f)) - ){ + modifier = Modifier + .padding(16.dp) + .clip(RoundedCornerShape(8.dp)) + .shadow( + elevation = 20.dp + ) + .background(Color.White.copy(alpha = 0.7f)) + ) { Icon( painter = painterResource(id = R.drawable.rider_pro_more_horizon), contentDescription = "", @@ -191,7 +193,7 @@ fun ProfilePage() { com.aiosman.riderpro.ui.composables.DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, - width = 300, + width = 250, menuItems = listOf( MenuItem( stringResource(R.string.logout), @@ -217,7 +219,7 @@ fun ProfilePage() { } }, MenuItem( - "Favourite", + stringResource(R.string.favourites), R.drawable.rider_pro_favourite ) { expanded = false @@ -227,7 +229,7 @@ fun ProfilePage() { } ), - ) + ) } } Spacer(modifier = Modifier.height(32.dp)) diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt index 6a76b56..e82bbcd 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikeNoticeViewModel.kt @@ -23,8 +23,12 @@ object LikeNoticeViewModel : ViewModel() { private val accountService: AccountService = AccountServiceImpl() private val _likeItemsFlow = MutableStateFlow>(PagingData.empty()) val likeItemsFlow = _likeItemsFlow.asStateFlow() - - init { + var isFirstLoad = true + fun reload(force: Boolean = false) { + if (!isFirstLoad && !force) { + return + } + isFirstLoad = false viewModelScope.launch { Pager( config = PagingConfig(pageSize = 5, enablePlaceholders = false), diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt index d875630..265996f 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt @@ -56,8 +56,10 @@ fun LikeNoticeScreen() { var dataFlow = model.likeItemsFlow var likes = dataFlow.collectAsLazyPagingItems() LaunchedEffect(Unit) { + model.reload() model.updateNotice() } + StatusBarMaskLayout( darkIcons = true, maskBoxBackgroundColor = Color(0xFFFFFFFF) @@ -125,7 +127,7 @@ fun ActionPostNoticeItem( val context = LocalContext.current val navController = LocalNavController.current Box( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) + modifier = Modifier.padding(vertical = 16.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -188,7 +190,7 @@ fun LikeCommentNoticeItem( val navController = LocalNavController.current val context = LocalContext.current Box( - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp).noRippleClickable { + modifier = Modifier.padding(vertical = 16.dp).noRippleClickable { item.comment?.postId.let { navController.navigate( NavigationRoute.Post.route.replace( @@ -261,7 +263,8 @@ fun LikeCommentNoticeItem( Text( text = item.comment?.content ?: "", fontSize = 12.sp, - color = Color(0x99000000) + color = Color(0x99000000), + maxLines = 2 ) } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt index 7f16145..55d8cce 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/login/emailsignup.kt @@ -53,9 +53,9 @@ import kotlinx.coroutines.launch @Composable fun EmailSignupScreen() { - var email by remember { mutableStateOf("takayamaaren@gmail.com") } - var password by remember { mutableStateOf("Dzh17217.") } - var confirmPassword by remember { mutableStateOf("Dzh17217.") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } var rememberMe by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) } var acceptPromotions by remember { mutableStateOf(false) } @@ -68,7 +68,6 @@ fun EmailSignupScreen() { var confirmPasswordError by remember { mutableStateOf(null) } var termsError by remember { mutableStateOf(false) } var promotionsError by remember { mutableStateOf(false) } - fun validateForm(): Boolean { emailError = when { // 非空 diff --git a/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt b/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt index 6015312..5a1fb08 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/login/userauth.kt @@ -93,7 +93,13 @@ fun UserAuthScreen() { } } catch (e: ServiceException) { // handle error - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + + if (e.code == 12005) { + emailError = context.getString(R.string.error_invalidate_username_password) + passwordError = context.getString(R.string.error_invalidate_username_password) + } else { + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + } } } @@ -181,26 +187,47 @@ fun UserAuthScreen() { Row( verticalAlignment = Alignment.CenterVertically ) { - CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { - Checkbox( - checked = rememberMe, - onCheckedChange = { - rememberMe = it - }, - colors = CheckboxDefaults.colors( - checkedColor = Color.Black - ), - ) - Text( - stringResource(R.string.remember_me), - modifier = Modifier.padding(start = 8.dp), - fontSize = 12.sp - ) - Spacer(modifier = Modifier.weight(1f)) - Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable { + com.aiosman.riderpro.ui.composables.Checkbox( + checked = rememberMe, + onCheckedChange = { + rememberMe = it + }, + size = 18 + ) + Text( + stringResource(R.string.remember_me), + modifier = Modifier.padding(start = 8.dp), + fontSize = 12.sp + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + stringResource(R.string.forgot_password), + fontSize = 12.sp, + modifier = Modifier.noRippleClickable { navController.navigate(NavigationRoute.ResetPassword.route) - }) - } + } + ) +// CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { +// Checkbox( +// checked = rememberMe, +// onCheckedChange = { +// rememberMe = it +// }, +// colors = CheckboxDefaults.colors( +// checkedColor = Color.Black +// ), +// ) +// Text( +// stringResource(R.string.remember_me), +// modifier = Modifier.padding(start = 8.dp), +// fontSize = 12.sp +// ) +// Spacer(modifier = Modifier.weight(1f)) +// Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable { +// navController.navigate(NavigationRoute.ResetPassword.route) +// }) +// } + } Spacer(modifier = Modifier.height(64.dp)) ActionButton( diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt index eb8d546..0f59d4d 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/CommentsViewModel.kt @@ -29,6 +29,7 @@ class CommentsViewModel( val commentsFlow = _commentsFlow.asStateFlow() var order: String by mutableStateOf("like") var addedCommentList by mutableStateOf>(emptyList()) + var subCommentLoadingMap by mutableStateOf(mutableMapOf()) /** * 预加载,在跳转到 PostScreen 之前设置好内容 @@ -49,15 +50,19 @@ class CommentsViewModel( fun reloadComment() { viewModelScope.launch { - Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), - pagingSourceFactory = { - CommentPagingSource( - CommentRemoteDataSource(commentService), - postId = postId.toInt(), - order = order - ) - }).flow.cachedIn(viewModelScope).collectLatest { - _commentsFlow.value = it + try { + Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + CommentPagingSource( + CommentRemoteDataSource(commentService), + postId = postId.toInt(), + order = order + ) + }).flow.cachedIn(viewModelScope).collectLatest { + _commentsFlow.value = it + } + } catch (e: Exception) { + e.printStackTrace() } } } @@ -144,7 +149,12 @@ class CommentsViewModel( fun deleteComment(commentId: Int) { viewModelScope.launch { commentService.DeleteComment(commentId) - reloadComment() + // 如果是刚刚创建的评论,则从addedCommentList中删除 + if (addedCommentList.any { it.id == commentId }) { + addedCommentList = addedCommentList.filter { it.id != commentId } + } else { + reloadComment() + } } } @@ -153,16 +163,23 @@ class CommentsViewModel( val currentPagingData = commentsFlow.value val updatedPagingData = currentPagingData.map { comment -> if (comment.id == commentId) { - val subCommentList = commentService.getComments( - postId = postId.toInt(), - parentCommentId = commentId, - pageNumber = comment.replyPage + 1, - pageSize = 3, - ).list - return@map comment.copy( - reply = comment.reply.plus(subCommentList), - replyPage = comment.replyPage + 1 - ) + try { + subCommentLoadingMap[commentId] = true + val subCommentList = commentService.getComments( + postId = postId.toInt(), + parentCommentId = commentId, + pageNumber = comment.replyPage + 1, + pageSize = 3, + ).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 } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt index d316790..8cca3d9 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPost.kt @@ -1,12 +1,11 @@ package com.aiosman.riderpro.ui.post -import android.app.Activity -import android.content.Intent -import android.util.Log +import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -14,10 +13,10 @@ 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.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow +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 @@ -25,9 +24,12 @@ 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.LinearProgressIndicator +import androidx.compose.material.MaterialTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text @@ -41,7 +43,9 @@ 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.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale @@ -50,17 +54,19 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider import androidx.lifecycle.viewModelScope -import coil.compose.AsyncImage import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.composables.DraggableGrid import com.aiosman.riderpro.ui.composables.RelPostCard import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.launch +import java.io.File @Preview @@ -76,7 +82,9 @@ fun NewPostScreen() { } StatusBarMaskLayout( darkIcons = true, - modifier = Modifier.fillMaxSize().background(Color.White) + modifier = Modifier + .fillMaxSize() + .background(Color.White) ) { Column( modifier = Modifier @@ -94,14 +102,19 @@ fun NewPostScreen() { NewPostTextField("Share your adventure…", NewPostViewModel.textContent) { NewPostViewModel.textContent = it } - Column ( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + 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) + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(color = Color(0xFFEEEEEE)) + .padding(24.dp) ) { RelPostCard( momentEntity = it, @@ -188,6 +201,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) { } } } + @Composable fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Unit) { @@ -211,7 +225,8 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni } } -@OptIn(ExperimentalLayoutApi::class) + + @Composable fun AddImageGrid() { val navController = LocalNavController.current @@ -226,44 +241,97 @@ fun AddImageGrid() { } } + val takePictureLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture() + ) { success -> + if (success) { + model.imageUriList += model.currentPhotoUri.toString() + } + } + val stroke = Stroke( width = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) ) - Box( - modifier = Modifier.fillMaxWidth() - ) { - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(18.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - model.imageUriList.forEach { - CustomAsyncImage( - context, - it, - contentDescription = "Image", - modifier = Modifier - .size(110.dp) + DraggableGrid( + items = NewPostViewModel.imageUriList, + onMove = { from, to -> + NewPostViewModel.imageUriList = NewPostViewModel.imageUriList.toMutableList().apply { + add(to, removeAt(from)) + } + }, + lockedIndices = listOf( - .drawBehind { - drawRoundRect(color = Color(0xFF999999), style = stroke) - }.noRippleClickable { - navController.navigate(NavigationRoute.NewPostImageGrid.route) - }, - contentScale = ContentScale.Crop + ), + onDragModeEnd = {}, + onDragModeStart = {}, + additionalItems = listOf( + + ), + + ) { item, isDrag -> + Box( + modifier = Modifier + ) { + CustomAsyncImage( + LocalContext.current, + item, + 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), + modifier = Modifier + .fillMaxWidth() + .padding(18.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { +// items(model.imageUriList.size) { index -> +// val uri = model.imageUriList[index] +// Box( +// modifier = Modifier +// .drawBehind { +// drawRoundRect(color = Color(0xFF999999), style = stroke) +// } +// ) { +// CustomAsyncImage( +// context, +// uri, +// contentDescription = "Image", +// modifier = Modifier +// .fillMaxWidth().aspectRatio(1f) +// .noRippleClickable { +// navController.navigate(NavigationRoute.NewPostImageGrid.route) +// }, +// contentScale = ContentScale.Crop +// ) +// } +// } + item { Box( modifier = Modifier - .size(110.dp) - + .fillMaxWidth() + .aspectRatio(1f) .drawBehind { drawRoundRect(color = Color(0xFF999999), style = stroke) } - .noRippleClickable{ + .noRippleClickable { pickImagesLauncher.launch("image/*") }, ) { @@ -275,10 +343,37 @@ fun AddImageGrid() { .align(Alignment.Center) ) } - + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .drawBehind { + drawRoundRect(color = Color(0xFF999999), style = stroke) + } + .noRippleClickable { + val photoFile = File(context.cacheDir, "photo.jpg") + val photoUri: Uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + photoFile + ) + model.currentPhotoUri = photoUri + takePictureLauncher.launch(photoUri) + }, + ) { + Image( + painter = painterResource(id = R.drawable.rider_pro_camera), + contentDescription = "Take Photo", + modifier = Modifier + .size(48.dp) + .align(Alignment.Center), + colorFilter = ColorFilter.tint(Color.Gray) + ) + } } } - } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt index e28049f..e827171 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/NewPostViewModel.kt @@ -29,6 +29,7 @@ object NewPostViewModel : ViewModel() { var imageUriList by mutableStateOf(listOf()) var relPostId by mutableStateOf(null) var relMoment by mutableStateOf(null) + var currentPhotoUri: Uri? = null fun asNewPost() { textContent = "" searchPlaceAddressResult = null diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt index e92b84e..40a0a41 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt @@ -1,7 +1,9 @@ package com.aiosman.riderpro.ui.post -import android.util.Log +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -14,7 +16,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -24,15 +25,11 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.LinearProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet @@ -46,22 +43,23 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow 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.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -69,7 +67,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.compose.LazyPagingItems +import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.AppState import com.aiosman.riderpro.LocalAnimatedContentScope @@ -82,12 +80,14 @@ import com.aiosman.riderpro.entity.MomentImageEntity import com.aiosman.riderpro.exp.formatPostTime import com.aiosman.riderpro.exp.timeAgo import com.aiosman.riderpro.ui.NavigationRoute -import com.aiosman.riderpro.ui.comment.CommentModalViewModel +import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.composables.CustomClickableText import com.aiosman.riderpro.ui.composables.EditCommentBottomModal +import com.aiosman.riderpro.ui.composables.FollowButton import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.modifiers.noRippleClickable @@ -113,28 +113,67 @@ fun PostScreen( var contextComment by remember { mutableStateOf(null) } var replyComment by remember { mutableStateOf(null) } var showCommentModal by remember { mutableStateOf(false) } + var commentModalState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var editCommentModalState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) LaunchedEffect(Unit) { viewModel.initData() } + if (showCommentMenu) { ModalBottomSheet( onDismissRequest = { showCommentMenu = false }, containerColor = Color.White, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ), + sheetState = commentModalState, dragHandle = {}, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), windowInsets = WindowInsets(0) ) { CommentMenuModal( onDeleteClick = { - showCommentMenu = false + scope.launch { + commentModalState.hide() + showCommentMenu = false + } contextComment?.let { viewModel.deleteComment(it.id) } + + }, + commentEntity = contextComment, + onCloseClick = { + scope.launch { + commentModalState.hide() + showCommentMenu = false + } + }, + isSelf = AppState.UserId?.toLong() == contextComment?.author, + onLikeClick = { + scope.launch { + commentModalState.hide() + showCommentMenu = false + } + contextComment?.let { + viewModel.viewModelScope.launch { + if (it.liked) { + viewModel.unlikeComment(it.id) + } else { + viewModel.likeComment(it.id) + } + } + } + + }, + onReplyClick = { + scope.launch { + commentModalState.hide() + showCommentMenu = false + replyComment = contextComment + showCommentModal = true + } } ) } @@ -177,6 +216,8 @@ fun PostScreen( content = it ) } + + editCommentModalState.hide() showCommentModal = false } @@ -186,31 +227,34 @@ fun PostScreen( Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { - PostBottomBar( - onLikeClick = { - scope.launch { - if (viewModel.moment?.liked == true) { - viewModel.dislikeMoment() - } else { - viewModel.likeMoment() + if (!viewModel.isError) { + PostBottomBar( + onLikeClick = { + scope.launch { + if (viewModel.moment?.liked == true) { + viewModel.dislikeMoment() + } else { + viewModel.likeMoment() + } } - } - }, - onCreateCommentClick = { - replyComment = null - showCommentModal = true - }, - onFavoriteClick = { - scope.launch { - if (viewModel.moment?.isFavorite == true) { - viewModel.unfavoriteMoment() - } else { - viewModel.favoriteMoment() + }, + onCreateCommentClick = { + replyComment = null + showCommentModal = true + }, + onFavoriteClick = { + scope.launch { + if (viewModel.moment?.isFavorite == true) { + viewModel.unfavoriteMoment() + } else { + viewModel.favoriteMoment() + } } - } - }, - momentEntity = viewModel.moment - ) + }, + momentEntity = viewModel.moment + ) + } + } ) { it @@ -220,96 +264,119 @@ fun PostScreen( .background(Color.White) ) { StatusBarSpacer() - Header( - avatar = viewModel.avatar, - nickname = viewModel.nickname, - userId = viewModel.moment?.authorId, - isFollowing = viewModel.accountProfileEntity?.isFollowing ?: false, - onFollowClick = { - scope.launch { - if (viewModel.accountProfileEntity?.isFollowing == true) { - viewModel.unfollowUser() - } else { - viewModel.followUser() - } - } - }, - onDeleteClick = { - viewModel.deleteMoment { - navController.popBackStack() - } + if (viewModel.isError) { + Box( + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + NoticeScreenHeader("Post", moreIcon = false) } - ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(383f / 527f) - ) { - PostImageView( - viewModel.moment?.images ?: emptyList() + + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Umm, post are not found.", + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 16.sp ) - - } - PostDetails( - viewModel.moment - ) - Spacer(modifier = Modifier.height(16.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(1.dp) - .background(Color(0xFFF7F7F7)) - ) { - - } - Spacer(modifier = Modifier.height(24.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource( - R.string.comment_count, - (viewModel.moment?.commentCount ?: 0) - ), fontSize = 14.sp - ) - Spacer(modifier = Modifier.weight(1f)) - OrderSelectionComponent() { - commentsViewModel.order = it - viewModel.reloadComment() - } - } - - Spacer(modifier = Modifier.height(16.dp)) - } - item { - CommentContent( - viewModel = commentsViewModel, - onLongClick = { - showCommentMenu = true - contextComment = it - }, - onReply = { parentComment, _, _, _ -> - replyComment = parentComment - showCommentModal = true - } ) } - item { - Spacer(modifier = Modifier.height(120.dp)) - } + }else{ + Header( + avatar = viewModel.avatar, + nickname = viewModel.nickname, + userId = viewModel.moment?.authorId, + isFollowing = viewModel.moment?.followStatus == true, + onFollowClick = { + scope.launch { + if (viewModel.moment?.followStatus == true) { + viewModel.unfollowUser() + } else { + viewModel.followUser() + } + } + }, + onDeleteClick = { + viewModel.deleteMoment { + navController.popBackStack() + } + } + ) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(383f / 527f) + ) { + PostImageView( + viewModel.moment?.images ?: emptyList() + ) + } + PostDetails( + viewModel.moment + ) + Spacer(modifier = Modifier.height(16.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(1.dp) + .background(Color(0xFFF7F7F7)) + ) { + + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource( + R.string.comment_count, + (viewModel.moment?.commentCount ?: 0) + ), fontSize = 14.sp + ) + Spacer(modifier = Modifier.weight(1f)) + OrderSelectionComponent() { + commentsViewModel.order = it + viewModel.reloadComment() + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + item { + CommentContent( + viewModel = commentsViewModel, + onLongClick = { comment -> + showCommentMenu = true + contextComment = comment + }, + onReply = { parentComment, _, _, _ -> + replyComment = parentComment + showCommentModal = true + } + ) + } + item { + Spacer(modifier = Modifier.height(120.dp)) + } + + } } + } } @@ -327,41 +394,43 @@ fun CommentContent( } for (item in addedTopLevelComment) { - Box( - modifier = Modifier.padding(horizontal = 16.dp) + AnimatedVisibility( + visible = true, + enter = fadeIn() + slideInVertically() ) { - CommentItem( - item, - onLike = { comment -> - viewModel.viewModelScope.launch { - if (comment.liked) { - viewModel.unlikeComment(comment.id) - } else { - viewModel.likeComment(comment.id) + Box( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + CommentItem( + item, + onLike = { comment -> + viewModel.viewModelScope.launch { + if (comment.liked) { + viewModel.unlikeComment(comment.id) + } else { + viewModel.likeComment(comment.id) + } } - } - }, - onLongClick = { - if (AppState.UserId != item.id) { - return@CommentItem - } - onLongClick(item) - }, - onReply = { parentComment, _, _, _ -> - onReply( - parentComment, - parentComment.author, - parentComment.name, - parentComment.avatar - ) - }, - onLoadMoreSubComments = { - viewModel.viewModelScope.launch { - viewModel.loadMoreSubComments(it.id) - } - }, - addedCommentList = viewModel.addedCommentList - ) + }, + onLongClick = { comment -> + onLongClick(comment) + }, + onReply = { parentComment, _, _, _ -> + onReply( + parentComment, + parentComment.author, + parentComment.name, + parentComment.avatar + ) + }, + onLoadMoreSubComments = { + viewModel.viewModelScope.launch { + viewModel.loadMoreSubComments(it.id) + } + }, + addedCommentList = viewModel.addedCommentList + ) + } } } @@ -381,11 +450,8 @@ fun CommentContent( } } }, - onLongClick = { - if (AppState.UserId != item.id) { - return@CommentItem - } - onLongClick(item) + onLongClick = { comment -> + onLongClick(comment) }, onReply = { parentComment, _, _, _ -> onReply( @@ -404,6 +470,67 @@ fun CommentContent( ) } } + if (commentsPagging.loadState.refresh is LoadState.Loading) { + Box( + modifier = Modifier.fillMaxSize().height(120.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + LinearProgressIndicator( + modifier = Modifier.width(160.dp), + color = Color(0xFFDA3832) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Loading...", + fontSize = 14.sp + ) + } + + } + return + } + if (commentsPagging.loadState.append is LoadState.Loading) { + Box( + modifier = Modifier.fillMaxSize().height(64.dp), + contentAlignment = Alignment.Center + ) { + LinearProgressIndicator( + modifier = Modifier.width(160.dp), + color = Color(0xFFDA3832) + ) + } + } + if (commentsPagging.loadState.refresh is LoadState.Error) { + Box( + modifier = Modifier.fillMaxSize().height(120.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "Failed to load comments,click to retry", + fontSize = 14.sp, + modifier = Modifier.noRippleClickable { + viewModel.reloadComment() + } + ) + } + } + if (commentsPagging.loadState.append is LoadState.Error) { + Box( + modifier = Modifier.fillMaxSize().height(64.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "Failed to load more comments,click to retry", + fontSize = 14.sp, + modifier = Modifier.noRippleClickable { + commentsPagging.retry() + } + ) + } + } } @@ -488,30 +615,12 @@ fun Header( Spacer(modifier = Modifier.width(8.dp)) Text(text = nickname ?: "", fontWeight = FontWeight.Bold) if (AppState.UserId != userId) { - Box( - modifier = Modifier - .height(20.dp) - .wrapContentWidth() - .padding(start = 6.dp) - .noRippleClickable { - onFollowClick() - }, - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.height(18.dp), - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "" - ) - Text( - text = if (isFollowing) stringResource(R.string.following_upper) else stringResource( - R.string.follow_upper - ), - fontSize = 12.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } + FollowButton( + isFollowing = isFollowing, + onFollowClick = onFollowClick, + imageModifier = Modifier.height(18.dp).width(80.dp), + fontSize = 12.sp + ) } if (AppState.UserId == userId) { Spacer(modifier = Modifier.weight(1f)) @@ -581,7 +690,7 @@ fun PostImageView( .fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - if(images.size > 1){ + if (images.size > 1) { images.forEachIndexed { index, _ -> Box( modifier = Modifier @@ -626,47 +735,6 @@ fun PostDetails( } } -@Composable -fun CommentsSection( - lazyPagingItems: LazyPagingItems, - scrollState: LazyListState = rememberLazyListState(), - onLike: (CommentEntity) -> Unit, - onLongClick: (CommentEntity) -> Unit, - onWillCollapse: (Boolean) -> Unit, -) { - LazyColumn( - state = scrollState, modifier = Modifier - .fillMaxHeight() - .padding(start = 16.dp, end = 16.dp) - ) { - items(lazyPagingItems.itemCount) { idx -> - val item = lazyPagingItems[idx] ?: return@items - CommentItem( - item, - onLike = { - onLike(item) - }, - onLongClick = { - onLongClick(item) - } - ) - } - } - - // Detect scroll direction and update showCollapseContent - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(scrollState) { - coroutineScope.launch { - snapshotFlow { scrollState.firstVisibleItemScrollOffset } - .collect { offset -> - Log.d("scroll", "offset: $offset") - onWillCollapse(offset == 0) - } - } - } -} - - @OptIn(ExperimentalFoundationApi::class) @Composable fun CommentItem( @@ -680,7 +748,7 @@ fun CommentItem( replyUserAvatar: String? ) -> Unit = { _, _, _, _ -> }, onLoadMoreSubComments: ((CommentEntity) -> Unit)? = {}, - onLongClick: () -> Unit = {}, + onLongClick: (CommentEntity) -> Unit = {}, addedCommentList: List = emptyList() ) { val context = LocalContext.current @@ -688,12 +756,6 @@ fun CommentItem( Column( modifier = Modifier .fillMaxWidth() - .combinedClickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = {}, - onLongClick = onLongClick - ) ) { Row(modifier = Modifier.padding(vertical = 8.dp)) { Box( @@ -719,7 +781,15 @@ fun CommentItem( } Spacer(modifier = Modifier.width(8.dp)) Column( - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onLongClick = { + onLongClick(commentEntity) + } + ) {} ) { Text(text = commentEntity.name, fontWeight = FontWeight.W600, fontSize = 14.sp) Row { @@ -741,33 +811,49 @@ fun CommentItem( pop() } append(" ${commentEntity.comment}") + } - ClickableText( - text = annotatedText, - onClick = { offset -> - annotatedText.getStringAnnotations( - tag = "replyUser", - start = offset, - end = offset - ).firstOrNull()?.let { - navController.navigate( - NavigationRoute.AccountProfile.route.replace( - "{id}", - it.item + Box { + CustomClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations( + tag = "replyUser", + start = offset, + end = offset + ).firstOrNull()?.let { + navController.navigate( + NavigationRoute.AccountProfile.route.replace( + "{id}", + it.item + ) ) - ) - } - }, - style = TextStyle(fontSize = 14.sp), - maxLines = Int.MAX_VALUE, - softWrap = true - ) + } + }, + style = TextStyle(fontSize = 14.sp), + onLongPress = { + onLongClick(commentEntity) + }, + ) + } + } else { Text( text = commentEntity.comment, fontSize = 14.sp, maxLines = Int.MAX_VALUE, - softWrap = true + softWrap = true, + modifier = Modifier.combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onLongClick = { + onLongClick( + commentEntity + ) + }, + ) { + + } ) } } @@ -780,7 +866,7 @@ fun CommentItem( Spacer(modifier = Modifier.width(8.dp)) if (AppState.UserId?.toLong() != commentEntity.author) { Text( - text = "Reply", + text = stringResource(R.string.reply), fontSize = 12.sp, color = Color.Gray, modifier = Modifier.noRippleClickable { @@ -832,13 +918,15 @@ fun CommentItem( isChild = true, onLike = onLike, onReply = onReply, - onLongClick = onLongClick + onLongClick = { comment -> + onLongClick(comment) + } ) } if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) { val remaining = commentEntity.replyCount - commentEntity.reply.size Text( - text = "View $remaining more replies", + text = stringResource(R.string.view_more_reply, remaining), fontSize = 12.sp, color = Color(0xFF6F94AE), modifier = Modifier.noRippleClickable { @@ -851,7 +939,6 @@ fun CommentItem( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostBottomBar( onCreateCommentClick: () -> Unit = {}, @@ -956,12 +1043,16 @@ fun PostMenuModal( onDeleteClick() } ) { - Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp)) + Image( + painter = painterResource(id = R.drawable.rider_pro_moment_delete), + contentDescription = "", + modifier = Modifier.size(24.dp) + ) } Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Delete", + text = stringResource(R.string.delete), fontSize = 11.sp, fontWeight = FontWeight.Bold ) @@ -971,43 +1062,165 @@ fun PostMenuModal( } @Composable -fun CommentMenuModal( - onDeleteClick: () -> Unit = {} +fun MenuActionItem( + icon: Int? = null, + text: String, + content: @Composable() (() -> Unit)? = null, + onClick: () -> Unit ) { Column( - modifier = Modifier - .fillMaxWidth() - .height(160.dp) - .padding(vertical = 47.dp, horizontal = 20.dp) + modifier = Modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Row( + Box( modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .noRippleClickable { - onDeleteClick() - } - ) { - Image(painter = painterResource(id = R.drawable.rider_pro_moment_delete), contentDescription = "",modifier = Modifier.size(24.dp)) + .clip(CircleShape) + .noRippleClickable { + onClick() } - - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Delete", - fontSize = 11.sp, - fontWeight = FontWeight.Bold + ) { + content?.invoke() + if (icon != null) { + Icon( + painter = painterResource(id = icon), + contentDescription = "", + modifier = Modifier.size(24.dp), + tint = Color.Black ) } } + + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = text, + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + } +} + +/** + * 评论菜单弹窗 + */ +@Composable +fun CommentMenuModal( + onDeleteClick: () -> Unit = {}, + commentEntity: CommentEntity? = null, + onCloseClick: () -> Unit = {}, + onLikeClick: () -> Unit = {}, + onReplyClick: () -> Unit = {}, + isSelf: Boolean = false +) { + val clipboard = LocalClipboardManager.current + + fun copyToClipboard() { + commentEntity?.let { + clipboard.setText( + AnnotatedString( + text = it.comment, + ) + ) + } + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 20.dp) + ) { + Text(stringResource(R.string.comment), fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(24.dp)) + commentEntity?.let { + Column( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xffeeeeee)) + + .padding(8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + ) { + CustomAsyncImage( + imageUrl = it.avatar, + modifier = Modifier.fillMaxSize(), + contentDescription = "Avatar", + ) + } + Spacer(modifier = Modifier.width(8.dp)) + androidx.compose.material.Text( + it.name, + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + Spacer(modifier = Modifier.height(4.dp)) + androidx.compose.material.Text( + it.comment, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(start = 32.dp), + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(32.dp)) + } + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + if (isSelf) { + MenuActionItem( + icon = R.drawable.rider_pro_moment_delete, + text = stringResource(R.string.delete) + ) { + onDeleteClick() + } + + Spacer(modifier = Modifier.width(48.dp)) + } + MenuActionItem( + icon = R.drawable.rider_pro_copy, + text = stringResource(R.string.copy) + ) { + copyToClipboard() + onCloseClick() + } + commentEntity?.let { + Spacer(modifier = Modifier.width(48.dp)) + MenuActionItem( + text = stringResource(R.string.like), + content = { + AnimatedLikeIcon( + liked = it.liked, + onClick = onLikeClick, + modifier = Modifier.size(24.dp) + ) + } + ) { + onCloseClick() + } + } + if (!isSelf) { + Spacer(modifier = Modifier.width(48.dp)) + MenuActionItem( + icon = R.drawable.rider_pro_comment, + text = stringResource(R.string.reply) + ) { + onReplyClick() + } + } + } + Spacer(modifier = Modifier.height(48.dp)) } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt index 4dd3eb0..1f537de 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/PostViewModel.kt @@ -28,24 +28,20 @@ class PostViewModel( var moment by mutableStateOf(null) var accountService: AccountService = AccountServiceImpl() var commentsViewModel: CommentsViewModel = CommentsViewModel(postId) + var isError by mutableStateOf(false) - /** - * 预加载,在跳转到 PostScreen 之前设置好内容 - */ - fun preTransit(momentEntity: MomentEntity?) { - this.moment = momentEntity - this.nickname = momentEntity?.nickname ?: "" - this.commentsViewModel = CommentsViewModel(postId) - commentsViewModel.preTransit() - } fun reloadComment() { commentsViewModel.reloadComment() } suspend fun initData() { - moment = service.getMomentById(postId.toInt()) -// accountProfileEntity = userService.getUserProfile(moment?.authorId.toString()) + try { + moment = service.getMomentById(postId.toInt()) + } catch (e: Exception) { + isError = true + return + } commentsViewModel.reloadComment() } @@ -106,16 +102,16 @@ class PostViewModel( } suspend fun followUser() { - accountProfileEntity?.let { - userService.followUser(it.id.toString()) - accountProfileEntity = accountProfileEntity?.copy(isFollowing = true) + moment?.let { + userService.followUser(it.authorId.toString()) + moment = moment?.copy(followStatus = true) } } suspend fun unfollowUser() { - accountProfileEntity?.let { - userService.unFollowUser(it.id.toString()) - accountProfileEntity = accountProfileEntity?.copy(isFollowing = false) + moment?.let { + userService.unFollowUser(it.authorId.toString()) + moment = moment?.copy(followStatus = false) } } diff --git a/app/src/main/res/drawable/rider_pro_copy.xml b/app/src/main/res/drawable/rider_pro_copy.xml new file mode 100644 index 0000000..fb84e00 --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_copy.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rider_pro_send_disable.xml b/app/src/main/res/drawable/rider_pro_send_disable.xml new file mode 100644 index 0000000..2e25ffc --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_send_disable.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2c7e47f..8de99af 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -57,4 +57,17 @@ 默认 最新 最早 + 下载 + 原始图片 + 收藏 + 删除 + 复制 + 点赞 + 回复 + 查看更多%1d条回复 + 错误的用户名或密码 + 找回密码 + 找回 + 邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。 + 邮件发送失败,请检查您的网络连接或稍后重试。 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e790b6..29f2fe3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,17 @@ Default Latest Earliest + Download + Original + Favourite + Delete + Copy + Like + Reply + View %1d more replies + Invalid email or password + RCOVER ACCOUNT + Recover + An email has been sent to your registered email address. Please check your inbox and follow the instructions to reset your password. + Failed to send email. Please check your network connection or try again later. \ No newline at end of file