调整细节

This commit is contained in:
2024-09-15 18:26:08 +08:00
parent 4b28a882a9
commit 6cf8e740dd
23 changed files with 341 additions and 156 deletions

View File

@@ -460,11 +460,17 @@ class AccountServiceImpl : AccountService {
} }
override suspend fun resetPassword(email: String) { override suspend fun resetPassword(email: String) {
ApiClient.api.resetPassword( val resp = ApiClient.api.resetPassword(
ResetPasswordRequestBody( ResetPasswordRequestBody(
username = email username = email
) )
) )
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to reset password")
}
} }
} }

View File

@@ -49,8 +49,17 @@ fun ResetPasswordScreen() {
var isSendSuccess by remember { mutableStateOf<Boolean?>(null) } var isSendSuccess by remember { mutableStateOf<Boolean?>(null) }
var isLoading by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) }
val navController = LocalNavController.current val navController = LocalNavController.current
var usernameError by remember { mutableStateOf<String?>(null) }
fun validate(): Boolean {
if (username.isEmpty()) {
usernameError = context.getString(R.string.text_error_email_required)
return false
}
usernameError = null
return true
}
fun resetPassword() { fun resetPassword() {
if (!validate()) return
scope.launch { scope.launch {
isLoading = true isLoading = true
try { try {
@@ -78,7 +87,7 @@ fun ResetPasswordScreen() {
) )
) { ) {
NoticeScreenHeader( NoticeScreenHeader(
"RECOVER ACCOUNT", stringResource(R.string.recover_account_upper),
moreIcon = false moreIcon = false
) )
} }
@@ -93,7 +102,7 @@ fun ResetPasswordScreen() {
if (isSendSuccess!!) { if (isSendSuccess!!) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Reset password email has been sent to your email address", text = stringResource(R.string.reset_mail_send_success),
style = TextStyle( style = TextStyle(
color = Color(0xFF333333), color = Color(0xFF333333),
fontSize = 14.sp, fontSize = 14.sp,
@@ -103,7 +112,7 @@ fun ResetPasswordScreen() {
} else { } else {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Failed to send reset password email", text = stringResource(R.string.reset_mail_send_failed),
style = TextStyle( style = TextStyle(
color = Color(0xFF333333), color = Color(0xFF333333),
fontSize = 14.sp, fontSize = 14.sp,
@@ -138,7 +147,8 @@ fun ResetPasswordScreen() {
onValueChange = { username = it }, onValueChange = { username = it },
label = stringResource(R.string.login_email_label), label = stringResource(R.string.login_email_label),
hint = stringResource(R.string.text_hint_email), hint = stringResource(R.string.text_hint_email),
enabled = !isLoading enabled = !isLoading,
error = usernameError
) )
Spacer(modifier = Modifier.height(72.dp)) Spacer(modifier = Modifier.height(72.dp))
if (isLoading) { if (isLoading) {
@@ -148,7 +158,7 @@ fun ResetPasswordScreen() {
modifier = Modifier modifier = Modifier
.width(345.dp) .width(345.dp)
.height(48.dp), .height(48.dp),
text = "Recover Account", text = stringResource(R.string.recover),
backgroundImage = R.mipmap.rider_pro_signup_red_bg backgroundImage = R.mipmap.rider_pro_signup_red_bg
) { ) {
resetPassword() resetPassword()

View File

@@ -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)
)
}
}

View File

@@ -1,5 +1,9 @@
package com.aiosman.riderpro.ui.composables 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.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -112,16 +116,23 @@ fun TextInputField(
.height(16.dp), .height(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (error != null) { AnimatedVisibility(
Image( visible = error != null,
painter = painterResource(id = R.mipmap.rider_pro_input_error), enter = fadeIn(),
contentDescription = "Error", exit = fadeOut()
modifier = Modifier.size(8.dp) ) {
) Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.size(4.dp)) Image(
Text(error, color = Color(0xFFE53935), fontSize = 12.sp) 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)
}
}
} }
} }
} }
} }

View File

@@ -30,6 +30,7 @@ fun FavouriteNoticeScreen() {
var dataFlow = model.favouriteItemsFlow var dataFlow = model.favouriteItemsFlow
var favourites = dataFlow.collectAsLazyPagingItems() var favourites = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
StatusBarMaskLayout( StatusBarMaskLayout(

View File

@@ -26,8 +26,13 @@ object FavouriteNoticeViewModel : ViewModel() {
private val _favouriteItemsFlow = private val _favouriteItemsFlow =
MutableStateFlow<PagingData<AccountFavouriteEntity>>(PagingData.empty()) MutableStateFlow<PagingData<AccountFavouriteEntity>>(PagingData.empty())
val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow() val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -47,7 +47,11 @@ fun FollowerListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
model.followUser(user.id) if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id)
}
} }
} }
} }

View File

@@ -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 currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user -> val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) { if (user.id == id) {
user.copy(isFollowing = true) user.copy(isFollowing = isFollow)
} else { } else {
user user
} }
@@ -60,4 +60,9 @@ object FollowerListViewModel : ViewModel() {
updateIsFollow(userId) updateIsFollow(userId)
} }
suspend fun unFollowUser(userId: Int) {
userService.unFollowUser(userId.toString())
updateIsFollow(userId, false)
}
} }

View File

@@ -25,12 +25,14 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -47,11 +49,14 @@ fun FollowerNoticeScreen() {
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems() var followers = dataFlow.collectAsLazyPagingItems()
Box( Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) { ) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
LazyColumn( LazyColumn(
@@ -114,30 +119,43 @@ fun FollowItem(
) { ) {
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp) Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp)
} }
if (!isFollowing) { if (userId != AppState.UserId) {
Box( FollowButton(
modifier = Modifier.noRippleClickable { isFollowing = isFollowing,
onFollow() onFollowClick = onFollow,
} fontSize = 14.sp,
) { imageModifier = Modifier
Image( .width(100.dp)
painter = painterResource(id = R.drawable.follow_bg), .height(24.dp)
contentDescription = "Follow", )
modifier = Modifier
.width(79.dp)
.height(24.dp)
)
Text(
"FOLLOW",
fontSize = 14.sp,
color = Color(0xFFFFFFFF),
modifier = Modifier.align(
Alignment.Center
)
)
}
} }
// 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
// )
// )
// }
} }
} }

View File

@@ -30,8 +30,13 @@ object FollowerNoticeViewModel : ViewModel() {
private val _followerItemsFlow = private val _followerItemsFlow =
MutableStateFlow<PagingData<AccountFollow>>(PagingData.empty()) MutableStateFlow<PagingData<AccountFollow>>(PagingData.empty())
val followerItemsFlow = _followerItemsFlow.asStateFlow() val followerItemsFlow = _followerItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -18,7 +18,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun FollowingListScreen(userId: Int) { fun FollowingListScreen(userId: Int) {
val model = FollowerListViewModel val model = FollowingListViewModel
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.loadData(userId) model.loadData(userId)
@@ -47,7 +47,11 @@ fun FollowingListScreen(userId: Int) {
isFollowing = user.isFollowing isFollowing = user.isFollowing
) { ) {
scope.launch { scope.launch {
model.followUser(user.id) if (user.isFollowing) {
model.unfollowUser(user.id)
} else {
model.followUser(user.id)
}
} }
} }
} }

View File

@@ -24,9 +24,6 @@ object FollowingListViewModel : ViewModel() {
val usersFlow = _usersFlow.asStateFlow() val usersFlow = _usersFlow.asStateFlow()
private var userId by mutableStateOf<Int?>(null) private var userId by mutableStateOf<Int?>(null)
fun loadData(id: Int) { fun loadData(id: Int) {
if (userId == id) {
return
}
userId = id userId = id
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
@@ -34,7 +31,7 @@ object FollowingListViewModel : ViewModel() {
pagingSourceFactory = { pagingSourceFactory = {
AccountPagingSource( AccountPagingSource(
userService, userService,
followerId = id followingId = id
) )
} }
).flow.cachedIn(viewModelScope).collectLatest { ).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 currentPagingData = usersFlow.value
val updatedPagingData = currentPagingData.map { user -> val updatedPagingData = currentPagingData.map { user ->
if (user.id == id) { if (user.id == id) {
user.copy(isFollowing = true) user.copy(isFollowing = isFollow)
} else { } else {
user user
} }
@@ -60,4 +57,9 @@ object FollowingListViewModel : ViewModel() {
updateIsFollow(userId) updateIsFollow(userId)
} }
suspend fun unfollowUser(userId: Int) {
userService.unFollowUser(userId.toString())
updateIsFollow(userId, false)
}
} }

View File

@@ -54,6 +54,9 @@ import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer 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.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.PostViewModel import com.aiosman.riderpro.ui.post.PostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -72,7 +75,7 @@ fun NotificationsScreen() {
var comments = dataFlow.collectAsLazyPagingItems() var comments = dataFlow.collectAsLazyPagingItems()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
MessageListViewModel.initData() MessageListViewModel.initData(force = true)
} }
}) })
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -103,6 +106,13 @@ fun NotificationsScreen() {
R.drawable.rider_pro_like, R.drawable.rider_pro_like,
stringResource(R.string.like_upper) stringResource(R.string.like_upper)
) { ) {
if (MessageListViewModel.likeNoticeCount > 0) {
// 刷新点赞消息列表
LikeNoticeViewModel.isFirstLoad = true
// 清除点赞消息数量
MessageListViewModel.clearLikeNoticeCount()
}
navController.navigate(NavigationRoute.Likes.route) navController.navigate(NavigationRoute.Likes.route)
} }
NotificationIndicator( NotificationIndicator(
@@ -110,6 +120,11 @@ fun NotificationsScreen() {
R.drawable.rider_pro_followers, R.drawable.rider_pro_followers,
stringResource(R.string.followers_upper) stringResource(R.string.followers_upper)
) { ) {
if (MessageListViewModel.followNoticeCount > 0) {
// 刷新关注消息列表
FollowerNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFollowNoticeCount()
}
navController.navigate(NavigationRoute.Followers.route) navController.navigate(NavigationRoute.Followers.route)
} }
NotificationIndicator( NotificationIndicator(
@@ -117,6 +132,11 @@ fun NotificationsScreen() {
R.drawable.rider_pro_favoriate, R.drawable.rider_pro_favoriate,
stringResource(R.string.favourites_upper) stringResource(R.string.favourites_upper)
) { ) {
if (MessageListViewModel.favouriteNoticeCount > 0) {
// 刷新收藏消息列表
FavouriteNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFavouriteNoticeCount()
}
navController.navigate(NavigationRoute.FavouritesScreen.route) navController.navigate(NavigationRoute.FavouritesScreen.route)
} }
} }

View File

@@ -31,9 +31,15 @@ object MessageListViewModel : ViewModel() {
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty()) private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentItemsFlow = _commentItemsFlow.asStateFlow() val commentItemsFlow = _commentItemsFlow.asStateFlow()
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var isFirstLoad = true
suspend fun initData() { suspend fun initData(force: Boolean = false) {
isLoading = true if (!isFirstLoad && !force) {
return
}
if (force) {
isLoading = true
}
isFirstLoad = false
val info = accountService.getMyNoticeInfo() val info = accountService.getMyNoticeInfo()
noticeInfo = info noticeInfo = info
viewModelScope.launch { viewModelScope.launch {
@@ -43,7 +49,7 @@ object MessageListViewModel : ViewModel() {
CommentPagingSource( CommentPagingSource(
CommentRemoteDataSource(commentService), CommentRemoteDataSource(commentService),
selfNotice = true, selfNotice = true,
order="latest" order = "latest"
) )
} }
).flow.cachedIn(viewModelScope).collectLatest { ).flow.cachedIn(viewModelScope).collectLatest {
@@ -51,6 +57,7 @@ object MessageListViewModel : ViewModel() {
} }
} }
isLoading = false isLoading = false
} }
val likeNoticeCount val likeNoticeCount
@@ -80,4 +87,15 @@ object MessageListViewModel : ViewModel() {
updateIsRead(id) updateIsRead(id)
} }
} }
fun clearLikeNoticeCount() {
noticeInfo = noticeInfo?.copy(likeCount = 0)
}
fun clearFollowNoticeCount() {
noticeInfo = noticeInfo?.copy(followCount = 0)
}
fun clearFavouriteNoticeCount() {
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
}
} }

View File

@@ -37,11 +37,16 @@ object MyProfileViewModel : ViewModel() {
private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty()) private var _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
var momentsFlow = _momentsFlow.asStateFlow() var momentsFlow = _momentsFlow.asStateFlow()
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var firstLoad = true
fun loadProfile(pullRefresh: Boolean = false) { fun loadProfile(pullRefresh: Boolean = false) {
if (!firstLoad && !pullRefresh) {
return
}
viewModelScope.launch { viewModelScope.launch {
if (pullRefresh){ if (pullRefresh){
refreshing = true refreshing = true
} }
firstLoad = false
profile = accountService.getMyAccountProfile() profile = accountService.getMyAccountProfile()
val profile = accountService.getMyAccountProfile() val profile = accountService.getMyAccountProfile()
refreshing = false refreshing = false

View File

@@ -23,8 +23,12 @@ object LikeNoticeViewModel : ViewModel() {
private val accountService: AccountService = AccountServiceImpl() private val accountService: AccountService = AccountServiceImpl()
private val _likeItemsFlow = MutableStateFlow<PagingData<AccountLikeEntity>>(PagingData.empty()) private val _likeItemsFlow = MutableStateFlow<PagingData<AccountLikeEntity>>(PagingData.empty())
val likeItemsFlow = _likeItemsFlow.asStateFlow() val likeItemsFlow = _likeItemsFlow.asStateFlow()
var isFirstLoad = true
init { fun reload(force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
isFirstLoad = false
viewModelScope.launch { viewModelScope.launch {
Pager( Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false), config = PagingConfig(pageSize = 5, enablePlaceholders = false),

View File

@@ -56,8 +56,10 @@ fun LikeNoticeScreen() {
var dataFlow = model.likeItemsFlow var dataFlow = model.likeItemsFlow
var likes = dataFlow.collectAsLazyPagingItems() var likes = dataFlow.collectAsLazyPagingItems()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload()
model.updateNotice() model.updateNotice()
} }
StatusBarMaskLayout( StatusBarMaskLayout(
darkIcons = true, darkIcons = true,
maskBoxBackgroundColor = Color(0xFFFFFFFF) maskBoxBackgroundColor = Color(0xFFFFFFFF)
@@ -125,7 +127,7 @@ fun ActionPostNoticeItem(
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
Box( Box(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) modifier = Modifier.padding(vertical = 16.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -188,7 +190,7 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp).noRippleClickable { modifier = Modifier.padding(vertical = 16.dp).noRippleClickable {
item.comment?.postId.let { item.comment?.postId.let {
navController.navigate( navController.navigate(
NavigationRoute.Post.route.replace( NavigationRoute.Post.route.replace(
@@ -261,7 +263,8 @@ fun LikeCommentNoticeItem(
Text( Text(
text = item.comment?.content ?: "", text = item.comment?.content ?: "",
fontSize = 12.sp, fontSize = 12.sp,
color = Color(0x99000000) color = Color(0x99000000),
maxLines = 2
) )
} }
} }

View File

@@ -53,9 +53,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun EmailSignupScreen() { fun EmailSignupScreen() {
var email by remember { mutableStateOf("takayamaaren@gmail.com") } var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("Dzh17217.") } var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("Dzh17217.") } var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) } var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) } var acceptPromotions by remember { mutableStateOf(false) }
@@ -68,7 +68,6 @@ fun EmailSignupScreen() {
var confirmPasswordError by remember { mutableStateOf<String?>(null) } var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var termsError by remember { mutableStateOf<Boolean>(false) } var termsError by remember { mutableStateOf<Boolean>(false) }
var promotionsError by remember { mutableStateOf<Boolean>(false) } var promotionsError by remember { mutableStateOf<Boolean>(false) }
fun validateForm(): Boolean { fun validateForm(): Boolean {
emailError = when { emailError = when {
// 非空 // 非空

View File

@@ -93,7 +93,13 @@ fun UserAuthScreen() {
} }
} catch (e: ServiceException) { } catch (e: ServiceException) {
// handle error // 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( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { com.aiosman.riderpro.ui.composables.Checkbox(
Checkbox( checked = rememberMe,
checked = rememberMe, onCheckedChange = {
onCheckedChange = { rememberMe = it
rememberMe = it },
}, size = 18
colors = CheckboxDefaults.colors( )
checkedColor = Color.Black Text(
), stringResource(R.string.remember_me),
) modifier = Modifier.padding(start = 8.dp),
Text( fontSize = 12.sp
stringResource(R.string.remember_me), )
modifier = Modifier.padding(start = 8.dp), Spacer(modifier = Modifier.weight(1f))
fontSize = 12.sp Text(
) stringResource(R.string.forgot_password),
Spacer(modifier = Modifier.weight(1f)) fontSize = 12.sp,
Text(stringResource(R.string.forgot_password), fontSize = 12.sp, modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ResetPassword.route) 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)) Spacer(modifier = Modifier.height(64.dp))
ActionButton( ActionButton(

View File

@@ -29,6 +29,7 @@ class CommentsViewModel(
val commentsFlow = _commentsFlow.asStateFlow() val commentsFlow = _commentsFlow.asStateFlow()
var order: String by mutableStateOf("like") var order: String by mutableStateOf("like")
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList()) var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
/** /**
* 预加载,在跳转到 PostScreen 之前设置好内容 * 预加载,在跳转到 PostScreen 之前设置好内容
@@ -162,16 +163,23 @@ class CommentsViewModel(
val currentPagingData = commentsFlow.value val currentPagingData = commentsFlow.value
val updatedPagingData = currentPagingData.map { comment -> val updatedPagingData = currentPagingData.map { comment ->
if (comment.id == commentId) { if (comment.id == commentId) {
val subCommentList = commentService.getComments( try {
postId = postId.toInt(), subCommentLoadingMap[commentId] = true
parentCommentId = commentId, val subCommentList = commentService.getComments(
pageNumber = comment.replyPage + 1, postId = postId.toInt(),
pageSize = 3, parentCommentId = commentId,
).list pageNumber = comment.replyPage + 1,
return@map comment.copy( pageSize = 3,
reply = comment.reply.plus(subCommentList), ).list
replyPage = comment.replyPage + 1 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 comment
} }

View File

@@ -29,8 +29,9 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -47,7 +48,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -67,6 +67,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalAnimatedContentScope import com.aiosman.riderpro.LocalAnimatedContentScope
@@ -79,22 +80,18 @@ import com.aiosman.riderpro.entity.MomentImageEntity
import com.aiosman.riderpro.exp.formatPostTime import com.aiosman.riderpro.exp.formatPostTime
import com.aiosman.riderpro.exp.timeAgo import com.aiosman.riderpro.exp.timeAgo
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder
import com.aiosman.riderpro.ui.composables.CustomAsyncImage 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.EditCommentBottomModal
import com.aiosman.riderpro.ui.composables.FollowButton
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.paging.LoadState
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomClickableText
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -618,31 +615,12 @@ fun Header(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = nickname ?: "", fontWeight = FontWeight.Bold) Text(text = nickname ?: "", fontWeight = FontWeight.Bold)
if (AppState.UserId != userId) { if (AppState.UserId != userId) {
Box( FollowButton(
modifier = Modifier isFollowing = isFollowing,
.height(20.dp) onFollowClick = onFollowClick,
.wrapContentWidth() imageModifier = Modifier.height(18.dp).width(80.dp),
.padding(start = 6.dp) fontSize = 12.sp
.noRippleClickable { )
onFollowClick()
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.height(18.dp).width(80.dp),
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 = 12.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
} }
if (AppState.UserId == userId) { if (AppState.UserId == userId) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -757,28 +735,6 @@ fun PostDetails(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LongPressClickableText(
text: AnnotatedString,
onClick: (Int) -> Unit,
onLongClick: () -> Unit,
style: TextStyle = TextStyle.Default
) {
ClickableText(
text = text,
onClick = onClick,
style = style,
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
onLongClick()
},
)
}
)
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CommentItem( fun CommentItem(
@@ -910,7 +866,7 @@ fun CommentItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
if (AppState.UserId?.toLong() != commentEntity.author) { if (AppState.UserId?.toLong() != commentEntity.author) {
Text( Text(
text = "Reply", text = stringResource(R.string.reply),
fontSize = 12.sp, fontSize = 12.sp,
color = Color.Gray, color = Color.Gray,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
@@ -970,7 +926,7 @@ fun CommentItem(
if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) { if (commentEntity.replyCount > 0 && !isChild && commentEntity.reply.size < commentEntity.replyCount) {
val remaining = commentEntity.replyCount - commentEntity.reply.size val remaining = commentEntity.replyCount - commentEntity.reply.size
Text( Text(
text = "View $remaining more replies", text = stringResource(R.string.view_more_reply, remaining),
fontSize = 12.sp, fontSize = 12.sp,
color = Color(0xFF6F94AE), color = Color(0xFF6F94AE),
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
@@ -1242,7 +1198,7 @@ fun CommentMenuModal(
commentEntity?.let { commentEntity?.let {
Spacer(modifier = Modifier.width(48.dp)) Spacer(modifier = Modifier.width(48.dp))
MenuActionItem( MenuActionItem(
text = "Like", text = stringResource(R.string.like),
content = { content = {
AnimatedLikeIcon( AnimatedLikeIcon(
liked = it.liked, liked = it.liked,
@@ -1258,7 +1214,7 @@ fun CommentMenuModal(
Spacer(modifier = Modifier.width(48.dp)) Spacer(modifier = Modifier.width(48.dp))
MenuActionItem( MenuActionItem(
icon = R.drawable.rider_pro_comment, icon = R.drawable.rider_pro_comment,
text = "Reply" text = stringResource(R.string.reply)
) { ) {
onReplyClick() onReplyClick()
} }

View File

@@ -61,4 +61,13 @@
<string name="original">原始图片</string> <string name="original">原始图片</string>
<string name="favourites">收藏</string> <string name="favourites">收藏</string>
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="copy">复制</string>
<string name="like">点赞</string>
<string name="reply">回复</string>
<string name="view_more_reply">查看更多%1d条回复</string>
<string name="error_invalidate_username_password">错误的用户名或密码</string>
<string name="recover_account_upper">找回密码</string>
<string name="recover">找回</string>
<string name="reset_mail_send_success">邮件已发送!请查收您的邮箱,按照邮件中的指示重置密码。</string>
<string name="reset_mail_send_failed">邮件发送失败,请检查您的网络连接或稍后重试。</string>
</resources> </resources>

View File

@@ -61,4 +61,12 @@
<string name="favourites">Favourite</string> <string name="favourites">Favourite</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>
<string name="like">Like</string>
<string name="reply">Reply</string>
<string name="view_more_reply">View %1d more replies</string>
<string name="error_invalidate_username_password">Invalid email or password</string>
<string name="recover_account_upper">RCOVER ACCOUNT</string>
<string name="recover">Recover</string>
<string name="reset_mail_send_success">An email has been sent to your registered email address. Please check your inbox and follow the instructions to reset your password.</string>
<string name="reset_mail_send_failed">Failed to send email. Please check your network connection or try again later.</string>
</resources> </resources>