This commit is contained in:
2024-09-27 21:29:30 +08:00
parent 24c0f57926
commit 2497698f27
11 changed files with 2122 additions and 228 deletions

View File

@@ -31,6 +31,7 @@ import com.aiosman.riderpro.ui.account.AccountEditScreen2
import com.aiosman.riderpro.ui.account.ResetPasswordScreen
import com.aiosman.riderpro.ui.chat.ChatScreen
import com.aiosman.riderpro.ui.comment.CommentsScreen
import com.aiosman.riderpro.ui.comment.notice.CommentNoticeScreen
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
import com.aiosman.riderpro.ui.follower.FollowerListScreen
@@ -86,6 +87,7 @@ sealed class NavigationRoute(
data object ResetPassword : NavigationRoute("ResetPassword")
data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
}
@@ -352,6 +354,13 @@ fun NavigationController(
ChatScreen(it.arguments?.getString("id")!!)
}
}
composable(route = NavigationRoute.CommentNoticeScreen.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
CommentNoticeScreen()
}
}
}

View File

@@ -0,0 +1,82 @@
package com.aiosman.riderpro.ui.comment.notice
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService
import com.aiosman.riderpro.data.CommentServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.entity.CommentEntity
import com.aiosman.riderpro.entity.CommentPagingSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class CommentNoticeListViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl()
val userService: UserService = UserServiceImpl()
private val commentService: CommentService = CommentServiceImpl()
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentItemsFlow = _commentItemsFlow.asStateFlow()
var isLoading by mutableStateOf(false)
var isFirstLoad = true
fun initData(context: Context, force: Boolean = false) {
if (!isFirstLoad && !force) {
return
}
if (force) {
isLoading = true
}
isFirstLoad = false
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
CommentPagingSource(
CommentRemoteDataSource(commentService),
selfNotice = true,
order = "latest"
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_commentItemsFlow.value = it
}
}
isLoading = false
}
private fun updateIsRead(id: Int) {
val currentPagingData = _commentItemsFlow.value
val updatedPagingData = currentPagingData.map { commentEntity ->
if (commentEntity.id == id) {
commentEntity.copy(unread = false)
} else {
commentEntity
}
}
_commentItemsFlow.value = updatedPagingData
}
fun updateReadStatus(id: Int) {
viewModelScope.launch {
commentService.updateReadStatus(id)
updateIsRead(id)
}
}
}

View File

@@ -2,12 +2,9 @@ package com.aiosman.riderpro.ui.index
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
@@ -31,18 +28,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.navigation.navOptions
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.index.tabs.add.AddPage
import com.aiosman.riderpro.ui.index.tabs.message.NotificationsScreen
import com.aiosman.riderpro.ui.index.tabs.moment.MomentsList
import com.aiosman.riderpro.ui.index.tabs.profile.ProfilePage
import com.aiosman.riderpro.ui.index.tabs.profile.v2.Profile2
import com.aiosman.riderpro.ui.index.tabs.profile.v2.Profile4
import com.aiosman.riderpro.ui.index.tabs.profile.v2.ProfilePage
import com.aiosman.riderpro.ui.index.tabs.search.DiscoverScreen
import com.aiosman.riderpro.ui.index.tabs.search.SearchScreen
import com.aiosman.riderpro.ui.index.tabs.shorts.ShortVideo
import com.aiosman.riderpro.ui.index.tabs.street.StreetPage
import com.aiosman.riderpro.ui.message.MessagePage
import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
@@ -213,7 +209,7 @@ fun Profile() {
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
ProfilePage()
Profile2()
}
}

View File

@@ -50,6 +50,7 @@ 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.navigateToChat
import com.aiosman.riderpro.ui.navigateToPost
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
@@ -63,16 +64,15 @@ import kotlinx.coroutines.launch
fun NotificationsScreen() {
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
var dataFlow = MessageListViewModel.commentItemsFlow
var comments = dataFlow.collectAsLazyPagingItems()
val context = LocalContext.current
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch {
MessageListViewModel.initData(force = true)
MessageListViewModel.initData(context, force = true)
}
})
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent)
MessageListViewModel.initData()
MessageListViewModel.initData(context)
}
Column(
modifier = Modifier.fillMaxSize()
@@ -118,108 +118,40 @@ fun NotificationsScreen() {
}
navController.navigate(NavigationRoute.Followers.route)
}
// NotificationIndicator(
// MessageListViewModel.favouriteNoticeCount,
// R.drawable.rider_pro_favoriate,
// stringResource(R.string.favourites_upper)
// ) {
// if (MessageListViewModel.favouriteNoticeCount > 0) {
// // 刷新收藏消息列表
// FavouriteNoticeViewModel.isFirstLoad = true
// MessageListViewModel.clearFavouriteNoticeCount()
// }
// navController.navigate(NavigationRoute.FavouritesScreen.route)
// }
NotificationIndicator(
MessageListViewModel.favouriteNoticeCount,
R.drawable.rider_pro_favoriate,
stringResource(R.string.favourites_upper)
MessageListViewModel.commentNoticeCount,
R.drawable.rider_pro_comment,
stringResource(R.string.comment).uppercase()
) {
if (MessageListViewModel.favouriteNoticeCount > 0) {
// 刷新收藏消息列表
FavouriteNoticeViewModel.isFirstLoad = true
MessageListViewModel.clearFavouriteNoticeCount()
}
navController.navigate(NavigationRoute.FavouritesScreen.route)
navController.navigate(NavigationRoute.CommentNoticeScreen.route)
}
}
HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp))
NotificationCounterItem(MessageListViewModel.commentNoticeCount)
if (comments.loadState.refresh is LoadState.Loading) {
NotificationCounterItem(MessageListViewModel.unReadConversationCount.toInt())
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 {
var highlightCommentId = comment.id
comment.parentCommentId?.let {
highlightCommentId = it
}
navController.navigateToPost(
id = comment.post!!.id,
highlightCommentId = highlightCommentId,
initImagePagerIndex = 0
)
}
}
}
}
// 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()
ChatMessageList(
MessageListViewModel.chatList,
onUserAvatarClick = { conv ->
MessageListViewModel.goToUserDetail(conv, navController)
},
contentAlignment = Alignment.Center
) {
Text(
text = "Load comment error, click to retry",
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(72.dp))
}
) { conv ->
MessageListViewModel.goToChat(conv, navController)
}
}
}
@@ -230,10 +162,7 @@ fun NotificationsScreen() {
)
}
}
}
@Composable
@@ -257,7 +186,7 @@ fun NotificationIndicator(
if (notificationCount > 0) {
Box(
modifier = Modifier
.background(Color(0xFFE53935), RoundedCornerShape(8.dp))
.background(Color(0xFFE53935), RoundedCornerShape(16.dp))
.padding(4.dp)
.align(Alignment.TopEnd)
) {
@@ -326,7 +255,6 @@ fun NotificationCounterItem(count: Int) {
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(4.dp)
)
}
}
@@ -335,57 +263,57 @@ fun NotificationCounterItem(count: Int) {
@Composable
fun CommentNoticeItem(
commentItem: CommentEntity,
onPostClick: () -> Unit = {},
fun ChatMessageList(
items: List<Conversation>,
onUserAvatarClick: (Conversation) -> Unit = {},
onChatClick: (Conversation) -> Unit = {}
) {
val navController = LocalNavController.current
val context = LocalContext.current
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(items.size) { index ->
val item = items[index]
Row(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
) {
Box {
CustomAsyncImage(
context = context,
imageUrl = commentItem.avatar,
contentDescription = commentItem.name,
context = LocalContext.current,
imageUrl = item.avatar,
contentDescription = item.nickname,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
commentItem.author.toString()
)
)
onUserAvatarClick(item)
}
)
}
Row(
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.noRippleClickable {
onPostClick()
onChatClick(item)
}
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = commentItem.name,
fontSize = 18.sp,
modifier = Modifier
)
Spacer(modifier = Modifier.height(4.dp))
Row {
var text = commentItem.comment
if (commentItem.parentCommentId != null) {
text = "Reply you: $text"
}
Text(
text = text,
text = item.nickname,
fontSize = 16.sp,
modifier = Modifier,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = item.lastMessageTime,
fontSize = 14.sp,
color = Color(0x66000000)
)
}
Spacer(modifier = Modifier.height(6.dp))
Row {
Text(
text = item.lastMessage,
fontSize = 14.sp,
maxLines = 1,
color = Color(0x99000000),
@@ -393,45 +321,25 @@ fun CommentNoticeItem(
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = commentItem.date.timeAgo(context),
fontSize = 14.sp,
color = Color(0x66000000)
)
}
}
Spacer(modifier = Modifier.width(24.dp))
commentItem.post?.let {
Box {
Box(
modifier = Modifier.padding(4.dp)
) {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Image",
modifier = Modifier
.size(48.dp)
)
// unread indicator
}
if (commentItem.unread) {
if (item.unreadCount > 0) {
Box(
modifier = Modifier
.background(Color(0xFFE53935), CircleShape)
.size(12.dp)
.align(Alignment.TopEnd)
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
Text(
text = item.unreadCount.toString(),
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Center)
)
}
}
Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
}
}
}

View File

@@ -1,10 +1,14 @@
package com.aiosman.riderpro.ui.index.tabs.message
import android.content.Context
import android.icu.util.Calendar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
@@ -18,21 +22,43 @@ import com.aiosman.riderpro.data.CommentRemoteDataSource
import com.aiosman.riderpro.data.CommentService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.CommentServiceImpl
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.exp.formatChatTime
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.navigateToChat
import com.aiosman.riderpro.utils.TrtcHelper
import com.tencent.imsdk.v2.V2TIMConversation
import com.tencent.imsdk.v2.V2TIMConversationResult
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
data class Conversation(
val trtcUserId: String,
val nickname: String,
val lastMessage: String,
val lastMessageTime: String,
val avatar: String = "",
val unreadCount: Int = 0
)
object MessageListViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl()
val userService: UserService = UserServiceImpl()
var noticeInfo by mutableStateOf<AccountNotice?>(null)
private val commentService: CommentService = CommentServiceImpl()
var chatList by mutableStateOf<List<Conversation>>(emptyList())
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
val commentItemsFlow = _commentItemsFlow.asStateFlow()
var isLoading by mutableStateOf(false)
var unReadConversationCount by mutableStateOf(0L)
var isFirstLoad = true
suspend fun initData(force: Boolean = false) {
suspend fun initData(context: Context, force: Boolean = false) {
loadChatList(context)
loadUnreadCount()
if (!isFirstLoad && !force) {
return
}
@@ -42,20 +68,7 @@ object MessageListViewModel : ViewModel() {
isFirstLoad = false
val info = accountService.getMyNoticeInfo()
noticeInfo = info
viewModelScope.launch {
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
CommentPagingSource(
CommentRemoteDataSource(commentService),
selfNotice = true,
order = "latest"
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_commentItemsFlow.value = it
}
}
isLoading = false
}
@@ -81,13 +94,6 @@ object MessageListViewModel : ViewModel() {
_commentItemsFlow.value = updatedPagingData
}
fun updateReadStatus(id: Int) {
viewModelScope.launch {
commentService.updateReadStatus(id)
updateIsRead(id)
updateUnReadCount(-1)
}
}
fun clearLikeNoticeCount() {
noticeInfo = noticeInfo?.copy(likeCount = 0)
@@ -96,18 +102,84 @@ object MessageListViewModel : ViewModel() {
fun clearFollowNoticeCount() {
noticeInfo = noticeInfo?.copy(followCount = 0)
}
fun clearFavouriteNoticeCount() {
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
}
fun updateUnReadCount(delta : Int) {
fun updateUnReadCount(delta: Int) {
noticeInfo?.let {
noticeInfo = it.copy(commentCount = it.commentCount + delta)
}
}
fun ResetModel(){
fun ResetModel() {
_commentItemsFlow.value = PagingData.empty()
noticeInfo = null
isLoading = false
isFirstLoad = true
}
suspend fun loadChatList(context: Context) {
val result = suspendCoroutine { continuation ->
V2TIMManager.getConversationManager().getConversationList(
0,
Int.MAX_VALUE,
object : V2TIMValueCallback<V2TIMConversationResult> {
override fun onSuccess(t: V2TIMConversationResult?) {
continuation.resumeWith(Result.success(t))
}
override fun onError(code: Int, desc: String?) {
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
}
}
)
}
chatList = result?.conversationList?.map { msg: V2TIMConversation ->
val lastMessage = Calendar.getInstance().apply {
timeInMillis = msg.lastMessage?.timestamp ?: 0
timeInMillis *= 1000
}
Conversation(
nickname = msg.showName,
lastMessage = msg.lastMessage?.textElem?.text ?: "",
lastMessageTime = lastMessage.time.formatChatTime(context),
avatar = msg.faceUrl,
unreadCount = msg.unreadCount,
trtcUserId = msg.userID
)
} ?: emptyList()
}
suspend fun loadUnreadCount() {
try {
this.unReadConversationCount = TrtcHelper.loadUnreadCount()
} catch (e: Exception) {
e.printStackTrace()
this.unReadConversationCount = 0
}
}
fun goToChat(
conversation: Conversation,
navController: NavHostController
) {
viewModelScope.launch {
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
navController.navigateToChat(profile.id.toString())
}
}
fun goToUserDetail(
conversation: Conversation,
navController: NavController
) {
viewModelScope.launch {
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
navController.navigate(NavigationRoute.AccountProfile.route.replace("{id}", profile.id.toString()))
}
}
}

View File

@@ -0,0 +1,116 @@
package com.aiosman.riderpro.ui.index.tabs.profile.v2
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.AppStore
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.MomentService
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.entity.MomentPagingSource
import com.aiosman.riderpro.entity.MomentRemoteDataSource
import com.aiosman.riderpro.entity.MomentServiceImpl
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
object MyProfileViewModel2 : ViewModel() {
val accountService: AccountService = AccountServiceImpl()
val momentService: MomentService = MomentServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null)
private var _sharedFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
var sharedFlow = _sharedFlow.asStateFlow()
var refreshing by mutableStateOf(false)
var firstLoad = true
fun loadProfile(pullRefresh: Boolean = false) {
viewModelScope.launch {
if (pullRefresh) {
refreshing = true
}
firstLoad = false
val profile = accountService.getMyAccountProfile()
MyProfileViewModel2.profile = profile
refreshing = false
try {
// Collect shared flow
Pager(
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
pagingSourceFactory = {
MomentPagingSource(
MomentRemoteDataSource(momentService),
author = profile.id
)
}
).flow.cachedIn(viewModelScope).collectLatest {
_sharedFlow.value = it
}
} catch (e: Exception) {
Log.e("MyProfileViewModel", "loadProfile: ", e)
}
}
}
suspend fun logout() {
AppStore.apply {
token = null
rememberMe = false
saveData()
}
AppState.ReloadAppState()
}
fun updateUserProfileBanner(bannerImageUrl: Uri?, context: Context) {
viewModelScope.launch {
var newBanner = bannerImageUrl?.let {
val cursor = context.contentResolver.query(it, null, null, null, null)
var newBanner: UploadImage? = null
cursor?.use { cur ->
if (cur.moveToFirst()) {
val displayName = cur.getString(cur.getColumnIndex("_display_name"))
val extension = displayName.substringAfterLast(".")
Log.d("NewPost", "File name: $displayName, extension: $extension")
// read as file
val file = uriToFile(context, it)
Log.d("NewPost", "File size: ${file.length()}")
newBanner = UploadImage(file, displayName, it.toString(), extension)
}
}
newBanner
}
accountService.updateProfile(
banner = newBanner,
avatar = null,
nickName = null,
bio = null
)
profile = accountService.getMyAccountProfile()
}
}
val bio get() = profile?.bio ?: ""
val nickName get() = profile?.nickName ?: ""
val avatar get() = profile?.avatar
fun ResetModel() {
profile = null
_sharedFlow.value = PagingData.empty()
firstLoad = true
}
}

View File

@@ -0,0 +1,542 @@
package com.aiosman.riderpro.ui.index.tabs.profile.v2
import android.app.Activity
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
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.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.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.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.AccountProfileEntity
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.MenuItem
import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Profile2() {
val model = MyProfileViewModel2
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
MyProfileViewModel2.loadProfile()
}
val navController: NavController = LocalNavController.current
val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MyProfileViewModel2.refreshing, onRefresh = {
MyProfileViewModel2.loadProfile(pullRefresh = true)
})
val context = LocalContext.current
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
val pickBannerImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
MyProfileViewModel2.updateUserProfileBanner(it, context = context)
}
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 处理滚动事件之前的逻辑
return Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 处理滚动事件之后的逻辑
return Offset.Zero
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.padding(bottom = with(LocalDensity.current) {
val da = WindowInsets.navigationBars
.getBottom(this)
.toDp() + 48.dp
da
})
.pullRefresh(state)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
) {
item {
Column(
modifier = Modifier
.fillMaxWidth()
) {
// banner
Box(
modifier = Modifier
.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(500.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
)
) // 添加阴影
) {
val banner = MyProfileViewModel2.profile?.banner
if (banner != null) {
CustomAsyncImage(
LocalContext.current,
banner,
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
} else {
Image(
painter = painterResource(id = R.drawable.rider_pro_moment_demo_2),
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 8.dp,
end = 8.dp
)
) {
Box(
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 = "",
modifier = Modifier.noRippleClickable {
expanded = true
},
tint = Color.Black
)
}
com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
expanded = false
scope.launch {
MyProfileViewModel2.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
},
MenuItem(
stringResource(R.string.favourites),
R.drawable.rider_pro_favourite
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
),
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
// 个人信息
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
MyProfileViewModel2.profile?.let {
UserItem(it)
}
}
Spacer(modifier = Modifier.height(16.dp))
MyProfileViewModel2.profile?.let {
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
SelfProfileAction {
navController.navigate(NavigationRoute.AccountEdit.route)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// 动态列表
}
UserContentTab(nestedScrollConnection)
}
}
PullRefreshIndicator(MyProfileViewModel2.refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
@Composable
fun UserItem(accountProfileEntity: AccountProfileEntity) {
Column(
modifier = Modifier
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
// 头像
CustomAsyncImage(
LocalContext.current,
accountProfileEntity.avatar,
modifier = Modifier
.clip(CircleShape)
.size(48.dp),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(32.dp))
//个人统计
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f)
) {
Text(
text = accountProfileEntity.followerCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.followers_upper),
)
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.following_upper),
)
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// 昵称
Text(
text = accountProfileEntity.nickName,
fontWeight = FontWeight.W600,
fontSize = 16.sp,
)
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
Text(
text = accountProfileEntity.bio,
fontSize = 14.sp,
color = Color.Gray
)
}
}
@Composable
fun SelfProfileAction(
onEditProfile: () -> Unit
) {
// 按钮
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(Color(0xffebebeb))
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
onEditProfile()
}
) {
Icon(
Icons.Default.Edit,
contentDescription = "",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.edit_profile),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun UserContentTab(nestedScrollConnection: NestedScrollConnection) {
val pagerState = rememberPagerState(pageCount = { 2 })
val scope = rememberCoroutineScope()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(if (pagerState.currentPage == 0) Color(0xFFFFFFFF) else Color.Transparent)
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
// switch to gallery
scope.launch {
pagerState.scrollToPage(0)
}
}
) {
Text(
text = "Gallery",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(if (pagerState.currentPage == 1) Color(0xFFFFFFFF) else Color.Transparent)
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
// switch to moments
scope.launch {
pagerState.scrollToPage(1)
}
}
) {
Text(
text = "Moments",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier
.padding(0.dp)
.height(900.dp),
beyondBoundsPageCount = 2,
verticalAlignment = Alignment.Top
) { page ->
when (page) {
0 -> Gallery()
1 -> MomentsList(nestedScrollConnection)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Gallery() {
val moments = MyProfileViewModel2.sharedFlow.collectAsLazyPagingItems()
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val itemWidth = (screenWidth / 2) - 16.dp - 2.dp
FlowRow(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
maxItemsInEachRow = 2,
) {
for (idx in 0 until moments.itemCount) {
val moment = moments[idx]
moment?.let {
Box(
modifier = Modifier
.width(itemWidth)
.aspectRatio(1f)
.background(Color.Gray)
) {
CustomAsyncImage(
LocalContext.current,
it.images[0].thumbnail,
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
}
BottomNavigationPlaceholder()
}
}
@Composable
fun MomentsList(nestedScrollConnection: NestedScrollConnection) {
val moments = MyProfileViewModel2.sharedFlow.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(900.dp).nestedScroll(nestedScrollConnection),
) {
item {
for (idx in 0 until moments.itemCount) {
val moment = moments[idx]
moment?.let {
MomentPostUnit(it)
}
}
}
}
}

View File

@@ -0,0 +1,134 @@
package com.aiosman.riderpro.ui.index.tabs.profile.v2
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun Profile3() {
Scaffold(
) { it
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
var headHeight by remember { mutableStateOf(400.dp) }
val speedFactor = 2 // 调整速度因子
// val animatedHeadHeight by animateDpAsState(
// targetValue = headHeight,
// animationSpec = tween(durationMillis = 0)
// )
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newHeight = (headHeight + delta.dp).coerceIn(0.dp, 400.dp)
headHeight = newHeight
return Offset.Zero
}
}
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
item {
// head
Box(
modifier = Modifier
.fillMaxWidth()
.height(headHeight)
.background(Color.DarkGray)
)
}
item {
LazyColumn(
modifier = Modifier.fillMaxWidth().height(screenHeight),
) {
items(100) { idx ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.background(Color.Green)
) {
Text("Item $idx")
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
// item {
// HorizontalPager(
// state = rememberPagerState(pageCount = { 2 }),
// modifier = Modifier
// .fillMaxWidth()
// .height(screenHeight)
// ) { page ->
// when (page) {
// 0 -> {
// LazyVerticalGrid(
// columns = GridCells.Fixed(2),
// verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
// horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
// modifier = Modifier.fillMaxSize(),
//// userScrollEnabled = headHeight < 2.dp
// ) {
// items(100) { idx ->
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(64.dp)
// ) {
// Text("Item $idx")
// }
// }
// }
// }
// 1 -> {
// LazyColumn(
// modifier = Modifier.fillMaxSize(),
//// userScrollEnabled = headHeight < 2.dp
// ) {
// items(100) { idx ->
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(64.dp)
// .background(Color.Green)
// ) {
// Text("Item $idx")
// }
// Spacer(modifier = Modifier.height(8.dp))
// }
// }
// }
// }
// }
// }
}
}
}

View File

@@ -0,0 +1,134 @@
package com.aiosman.riderpro.ui.index.tabs.profile.v2
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun Profile4() {
Scaffold(
) { it
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
var headHeight by remember { mutableStateOf(400.dp) }
val speedFactor = 2 // 调整速度因子
// val animatedHeadHeight by animateDpAsState(
// targetValue = headHeight,
// animationSpec = tween(durationMillis = 0)
// )
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newHeight = (headHeight + delta.dp).coerceIn(0.dp, 400.dp)
headHeight = newHeight
return Offset.Zero
}
}
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
item {
// head
Box(
modifier = Modifier
.fillMaxWidth()
.height(headHeight)
.background(Color.DarkGray)
)
}
item {
LazyColumn(
modifier = Modifier.fillMaxWidth().height(screenHeight),
) {
items(100) { idx ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.background(Color.Green)
) {
Text("Item $idx")
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
// item {
// HorizontalPager(
// state = rememberPagerState(pageCount = { 2 }),
// modifier = Modifier
// .fillMaxWidth()
// .height(screenHeight)
// ) { page ->
// when (page) {
// 0 -> {
// LazyVerticalGrid(
// columns = GridCells.Fixed(2),
// verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
// horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
// modifier = Modifier.fillMaxSize(),
//// userScrollEnabled = headHeight < 2.dp
// ) {
// items(100) { idx ->
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(64.dp)
// ) {
// Text("Item $idx")
// }
// }
// }
// }
// 1 -> {
// LazyColumn(
// modifier = Modifier.fillMaxSize(),
//// userScrollEnabled = headHeight < 2.dp
// ) {
// items(100) { idx ->
// Box(
// modifier = Modifier
// .fillMaxWidth()
// .height(64.dp)
// .background(Color.Green)
// ) {
// Text("Item $idx")
// }
// Spacer(modifier = Modifier.height(8.dp))
// }
// }
// }
// }
// }
// }
}
}
}

View File

@@ -0,0 +1,879 @@
package com.aiosman.riderpro.ui.index.tabs.profile.v2
import android.app.Activity
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.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.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.exp.formatPostTime2
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.navigateToChat
import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.riderpro.ui.post.NewPostViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ProfilePage() {
val model = MyProfileViewModel2
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
MyProfileViewModel.loadProfile()
}
val navController: NavController = LocalNavController.current
val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MyProfileViewModel.refreshing, onRefresh = {
MyProfileViewModel.loadProfile(pullRefresh = true)
})
val context = LocalContext.current
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
val pickBannerImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
MyProfileViewModel.updateUserProfileBanner(it, context = context)
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.padding(bottom = with(LocalDensity.current) {
val da = WindowInsets.navigationBars
.getBottom(this)
.toDp() + 48.dp
da
})
.pullRefresh(state)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
) {
val banner = MyProfileViewModel.profile?.banner
if (banner != null) {
CustomAsyncImage(
LocalContext.current,
banner,
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
} else {
Image(
painter = painterResource(id = R.drawable.rider_pro_moment_demo_2),
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 8.dp,
end = 8.dp
)
) {
Box(
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 = "",
modifier = Modifier.noRippleClickable {
expanded = true
},
tint = Color.Black
)
}
com.aiosman.riderpro.ui.composables.DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
expanded = false
scope.launch {
MyProfileViewModel.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}
},
MenuItem(
stringResource(R.string.change_password),
R.mipmap.rider_pro_change_password
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
},
MenuItem(
stringResource(R.string.favourites),
R.drawable.rider_pro_favourite
) {
expanded = false
scope.launch {
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
),
)
}
}
Spacer(modifier = Modifier.height(32.dp))
MyProfileViewModel.profile?.let {
UserInformation(
accountProfileEntity = it,
onEditProfileClick = {
navController.navigate(NavigationRoute.AccountEdit.route)
}
)
}
// if (moments.itemCount == 0) {
// EmptyMomentPostUnit()
// }
}
// items(moments.itemCount) { idx ->
// val momentItem = moments[idx] ?: return@items
// MomentPostUnit(momentItem)
// }
item {
Spacer(modifier = Modifier.height(48.dp))
}
}
PullRefreshIndicator(MyProfileViewModel.refreshing, state, Modifier.align(Alignment.TopCenter))
}
}
@Composable
fun CarGroup() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 54.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CarTopInformation()
CarTopPicture()
}
}
@Composable
fun CarTopInformation() {
Row {
Text(
text = "BMW",
color = Color.Black,
fontSize = 12.sp,
style = TextStyle(fontWeight = FontWeight.Bold)
)
Text(
modifier = Modifier.padding(start = 4.dp),
text = "/",
color = Color.Gray,
fontSize = 12.sp
)
Text(
modifier = Modifier.padding(start = 4.dp),
text = "M1000RR",
color = Color.Gray,
fontSize = 12.sp
)
}
}
@Composable
fun CarTopPicture() {
Image(
modifier = Modifier
.size(width = 336.dp, height = 224.dp)
.padding(top = 42.dp),
painter = painterResource(id = R.drawable.default_profile_moto), contentDescription = ""
)
}
@Composable
fun UserInformation(
isSelf: Boolean = true,
accountProfileEntity: AccountProfileEntity,
onFollowClick: () -> Unit = {},
onEditProfileClick: () -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 33.dp, end = 33.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(modifier = Modifier.fillMaxWidth()) {
val userInfoModifier = Modifier.weight(1f)
UserInformationFollowers(userInfoModifier, accountProfileEntity)
UserInformationBasic(userInfoModifier, accountProfileEntity)
UserInformationFollowing(userInfoModifier, accountProfileEntity)
}
UserInformationSlogan(accountProfileEntity)
CommunicationOperatorGroup(
isSelf = isSelf,
isFollowing = accountProfileEntity.isFollowing,
onFollowClick = onFollowClick,
onEditProfileClick = onEditProfileClick,
accountProfileEntity = accountProfileEntity
)
}
}
@Composable
fun UserInformationFollowers(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
val navController = LocalNavController.current
Column(modifier = modifier.padding(top = 31.dp)) {
Text(
modifier = Modifier
.padding(bottom = 5.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.FollowerList.route.replace(
"{id}",
accountProfileEntity.id.toString()
)
)
},
text = accountProfileEntity.followerCount.toString(),
fontSize = 24.sp,
color = Color.Black,
style = TextStyle(fontStyle = FontStyle.Italic, fontWeight = FontWeight.Bold)
)
Canvas(
modifier = Modifier
.size(width = 88.83.dp, height = 1.dp)
.padding(top = 2.dp, bottom = 5.dp)
) {
drawLine(
color = Color(0xFFCCCCCC),
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = 1f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Text(
modifier = Modifier.padding(top = 5.dp),
text = stringResource(R.string.followers_upper),
fontSize = 12.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
@Composable
fun UserInformationBasic(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
val context = LocalContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier.size(width = 112.dp, height = 112.dp),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.avatar_bold), contentDescription = ""
)
CustomAsyncImage(
context,
accountProfileEntity.avatar,
modifier = Modifier
.size(width = 88.dp, height = 88.dp)
.clip(
RoundedCornerShape(88.dp)
),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
Text(
modifier = Modifier
.widthIn(max = 220.dp)
.padding(top = 8.dp),
text = accountProfileEntity.nickName,
fontSize = 32.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center
)
Text(
modifier = Modifier.padding(top = 4.dp),
text = accountProfileEntity.country,
fontSize = 12.sp,
color = Color.Gray
)
}
}
@Composable
fun UserInformationFollowing(modifier: Modifier, accountProfileEntity: AccountProfileEntity) {
val navController = LocalNavController.current
Column(
modifier = modifier.padding(top = 6.dp),
horizontalAlignment = Alignment.End
) {
Text(
modifier = Modifier
.padding(bottom = 5.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.FollowingList.route.replace(
"{id}",
accountProfileEntity.id.toString()
)
)
},
text = accountProfileEntity.followingCount.toString(),
fontSize = 24.sp,
color = Color.Black,
style = TextStyle(fontStyle = FontStyle.Italic, fontWeight = FontWeight.Bold)
)
Canvas(
modifier = Modifier
.size(width = 88.83.dp, height = 1.dp)
.padding(top = 2.dp, bottom = 5.dp)
) {
drawLine(
color = Color(0xFFCCCCCC),
start = Offset(0f, 0f),
end = Offset(size.width, 0f),
strokeWidth = 1f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Text(
modifier = Modifier.padding(top = 5.dp),
text = stringResource(R.string.following_upper),
fontSize = 12.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
@Composable
fun UserInformationSlogan(accountProfileEntity: AccountProfileEntity) {
Text(
modifier = Modifier.padding(top = 23.dp),
text = accountProfileEntity.bio,
fontSize = 13.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
@Composable
fun CommunicationOperatorGroup(
accountProfileEntity: AccountProfileEntity,
isSelf: Boolean = true,
isFollowing: Boolean = false,
onFollowClick: () -> Unit = {},
onEditProfileClick: () -> Unit = {}
) {
val navController = LocalNavController.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp), horizontalArrangement = Arrangement.Center
) {
if (!isSelf && AppState.UserId != accountProfileEntity.id) {
Box(
modifier = Modifier
.size(width = 142.dp, height = 40.dp)
.noRippleClickable {
onFollowClick()
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = if (isFollowing) painterResource(id = R.mipmap.rider_pro_follow_grey) else painterResource(
id = R.mipmap.rider_pro_follow_red
),
contentDescription = ""
)
Text(
text = if (isFollowing) stringResource(R.string.following_upper) else stringResource(
R.string.follow_upper
),
fontSize = 14.sp,
color = if (isFollowing) Color.Black else Color.White,
style = TextStyle(fontWeight = FontWeight.W600, fontStyle = FontStyle.Italic),
)
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.size(width = 142.dp, height = 40.dp)
.noRippleClickable {
navController.navigateToChat(accountProfileEntity.id.toString())
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.mipmap.rider_pro_btn_bg_grey),
contentDescription = ""
)
Text(
text = "CHAT",
fontSize = 14.sp,
color = Color.Black,
fontWeight = FontWeight.Bold,
style = TextStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
)
}
}
if (isSelf) {
Box(
modifier = Modifier
.size(width = 142.dp, height = 40.dp)
.noRippleClickable {
onEditProfileClick()
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.mipmap.rider_pro_btn_bg_grey),
contentDescription = ""
)
Text(
text = stringResource(R.string.edit_profile),
fontSize = 14.sp,
color = Color.Black,
fontWeight = FontWeight.Bold,
style = TextStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
)
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RidingStyle() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = "RIDING STYLES",
fontSize = 18.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold)
)
Image(
modifier = Modifier
.padding(top = 4.dp)
.height(8.dp),
painter = painterResource(id = R.drawable.rider_pro_profile_line),
contentDescription = ""
)
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp)
) {
RidingStyleItem(styleContent = "Cruiser")
RidingStyleItem(styleContent = "Bobber")
RidingStyleItem(styleContent = "Cafe")
RidingStyleItem(styleContent = "Chopper")
RidingStyleItem(styleContent = "Sport")
RidingStyleItem(styleContent = "Vintage")
RidingStyleItem(styleContent = "Trike")
RidingStyleItem(styleContent = "Touring")
}
}
}
@Composable
fun RidingStyleItem(styleContent: String) {
Box(
modifier = Modifier.padding(bottom = 8.dp, end = 8.dp),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.shadow(
ambientColor = Color.Gray,
spotColor = Color(0f, 0f, 0f, 0.2f),
elevation = 20.dp,
),
painter = painterResource(id = R.drawable.rider_pro_style_wrapper),
contentDescription = ""
)
Text(
modifier = Modifier.padding(start = 5.dp, end = 5.dp),
text = styleContent,
fontSize = 12.sp,
color = Color.Gray,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
@Composable
fun EmptyMomentPostUnit() {
TimeGroup(stringResource(R.string.empty_my_post_title))
ProfileEmptyMomentCard()
}
@Composable
fun ProfileEmptyMomentCard(
) {
var columnHeight by remember { mutableStateOf(0) }
val navController = LocalNavController.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Canvas(
modifier = Modifier
.height(with(LocalDensity.current) { columnHeight.toDp() })
.width(14.dp)
) {
drawLine(
color = Color(0xff899DA9),
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.weight(1f)
.onGloballyPositioned { coordinates ->
columnHeight = coordinates.size.height
}
) {
Text(stringResource(R.string.empty_my_post_content), fontSize = 16.sp)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 2f)
.background(Color.White)
.padding(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.noRippleClickable {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
}
) {
Icon(
Icons.Default.Add,
tint = Color(0xFFD8D8D8),
contentDescription = "New post",
modifier = Modifier
.size(32.dp)
.align(Alignment.Center)
)
}
}
}
}
}
}
@Composable
fun MomentPostUnit(momentEntity: MomentEntity) {
TimeGroup(momentEntity.time.formatPostTime2())
ProfileMomentCard(
momentEntity.momentTextContent,
momentEntity.images[0].thumbnail,
momentEntity.likeCount.toString(),
momentEntity.commentCount.toString(),
momentEntity = momentEntity
)
}
@Composable
fun TimeGroup(time: String = "2024.06.08 12:23") {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.height(16.dp)
.width(14.dp),
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = time,
fontSize = 16.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.W600)
)
}
}
@Composable
fun ProfileMomentCard(
content: String,
imageUrl: String,
like: String,
comment: String,
momentEntity: MomentEntity
) {
var columnHeight by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Canvas(
modifier = Modifier
.height(with(LocalDensity.current) { columnHeight.toDp() })
.width(14.dp)
) {
drawLine(
color = Color(0xff899DA9),
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.background(Color.White)
.weight(1f)
.onGloballyPositioned { coordinates ->
columnHeight = coordinates.size.height
}
) {
if (content.isNotEmpty()) {
MomentCardTopContent(content)
}
MomentCardPicture(imageUrl, momentEntity = momentEntity)
MomentCardOperation(like, comment)
}
}
}
}
@Composable
fun MomentCardTopContent(content: String) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
text = content, fontSize = 16.sp, color = Color.Black
)
}
}
@Composable
fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
val navController = LocalNavController.current
val context = LocalContext.current
CustomAsyncImage(
context,
imageUrl,
modifier = Modifier
.fillMaxSize()
.aspectRatio(3f / 2f)
.padding(top = 16.dp)
.noRippleClickable {
navController.navigateToPost(
id = momentEntity.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
},
contentDescription = "",
contentScale = ContentScale.Crop
)
}
@Composable
fun MomentCardOperation(like: String, comment: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
// Spacer(modifier = Modifier.weight(1f))
MomentCardOperationItem(
drawable = R.drawable.rider_pro_like,
number = like,
modifier = Modifier.padding(end = 32.dp)
)
MomentCardOperationItem(
drawable = R.drawable.rider_pro_moment_comment,
number = comment,
modifier = Modifier.padding(end = 32.dp)
)
}
}
@Composable
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
painter = painterResource(id = drawable), contentDescription = ""
)
Text(text = number)
}
}

View File

@@ -0,0 +1,22 @@
package com.aiosman.riderpro.utils
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlin.coroutines.suspendCoroutine
object TrtcHelper {
suspend fun loadUnreadCount(): Long {
return suspendCoroutine { continuation ->
V2TIMManager.getConversationManager()
.getTotalUnreadMessageCount(object : V2TIMValueCallback<Long> {
override fun onSuccess(t: Long?) {
continuation.resumeWith(Result.success(t ?: 0))
}
override fun onError(code: Int, desc: String?) {
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
}
});
}
}
}