更新代码

This commit is contained in:
2024-10-07 10:12:41 +08:00
parent 7852d9de8c
commit 0f9923f3e0
17 changed files with 115 additions and 1011 deletions

View File

@@ -154,7 +154,7 @@ fun ClickCaptchaDialog(
onDismissRequest()
},
title = {
Text("Captcha")
Text(stringResource(R.string.captcha))
},
text = {
Column {
@@ -163,14 +163,14 @@ fun ClickCaptchaDialog(
.fillMaxWidth()
) {
ClickCaptchaView(
captchaData = captchaData!!,
captchaData = captchaData,
onPositionClicked = onPositionClicked
)
}
Spacer(modifier = Modifier.height(16.dp))
ActionButton(
text = "Refresh",
text = stringResource(R.string.refresh),
modifier = Modifier
.fillMaxWidth(),
) {

View File

@@ -96,7 +96,7 @@ fun CollapsingToolbarScaffold(
toolbarModifier: Modifier = Modifier,
toolbarClipToBounds: Boolean = true,
toolbarScrollable: Boolean = false,
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
toolbar: @Composable CollapsingToolbarScope.(ScrollState) -> Unit,
body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
) {
val flingBehavior = ScrollableDefaults.flingBehavior()
@@ -122,7 +122,7 @@ fun CollapsingToolbarScaffold(
toolbarState,
toolbarScrollState
)
toolbar()
toolbar(toolbarScrollState)
}
CollapsingToolbarScaffoldScopeInstance.body()

View File

@@ -1,5 +1,6 @@
package com.aiosman.riderpro.ui.composables.toolbar
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
@@ -14,7 +15,7 @@ fun ToolbarWithFabScaffold(
scrollStrategy: ScrollStrategy,
toolbarModifier: Modifier = Modifier,
toolbarClipToBounds: Boolean = true,
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
toolbar: @Composable CollapsingToolbarScope.(ScrollState) -> Unit,
toolbarScrollable: Boolean = false,
fab: @Composable () -> Unit,
fabPosition: FabPosition = FabPosition.End,

View File

@@ -1,12 +1,19 @@
package com.aiosman.riderpro.ui.follower
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -16,15 +23,21 @@ import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FollowerListScreen(userId: Int) {
val model = FollowerListViewModel
val scope = rememberCoroutineScope()
val refreshState = rememberPullRefreshState(model.isLoading, onRefresh = {
model.loadData(userId, true)
})
LaunchedEffect(Unit) {
model.loadData(userId)
}
StatusBarMaskLayout(
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
) {
var dataFlow = model.usersFlow
var users = dataFlow.collectAsLazyPagingItems()
@@ -35,27 +48,39 @@ fun FollowerListScreen(userId: Int) {
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LazyColumn(
modifier = Modifier.weight(1f)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.pullRefresh(refreshState)
) {
items(users.itemCount) { index ->
users[index]?.let { user ->
FollowItem(
avatar = user.avatar,
nickname = user.nickName,
userId = user.id,
isFollowing = user.isFollowing
) {
scope.launch {
if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(users.itemCount) { index ->
users[index]?.let { user ->
FollowItem(
avatar = user.avatar,
nickname = user.nickName,
userId = user.id,
isFollowing = user.isFollowing
) {
scope.launch {
if (user.isFollowing) {
model.unFollowUser(user.id)
} else {
model.followUser(user.id)
}
}
}
}
}
}
PullRefreshIndicator(
refreshing = model.isLoading,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}

View File

@@ -23,10 +23,12 @@ object FollowerListViewModel : ViewModel() {
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
val usersFlow = _usersFlow.asStateFlow()
private var userId by mutableStateOf<Int?>(null)
fun loadData(id: Int) {
if (userId == id) {
var isLoading by mutableStateOf(false)
fun loadData(id: Int,force : Boolean = false) {
if (userId == id && !force) {
return
}
isLoading = true
userId = id
viewModelScope.launch {
Pager(
@@ -41,6 +43,7 @@ object FollowerListViewModel : ViewModel() {
_usersFlow.value = it
}
}
isLoading = false
}
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {

View File

@@ -1,12 +1,18 @@
package com.aiosman.riderpro.ui.follower
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -16,10 +22,14 @@ import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FollowingListScreen(userId: Int) {
val model = FollowingListViewModel
val scope = rememberCoroutineScope()
val refreshState = rememberPullRefreshState(model.isLoading, onRefresh = {
model.loadData(userId, true)
})
LaunchedEffect(Unit) {
model.loadData(userId)
}
@@ -35,27 +45,39 @@ fun FollowingListScreen(userId: Int) {
) {
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
}
LazyColumn(
modifier = Modifier.weight(1f)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.pullRefresh(refreshState)
) {
items(users.itemCount) { index ->
users[index]?.let { user ->
FollowItem(
avatar = user.avatar,
nickname = user.nickName,
userId = user.id,
isFollowing = user.isFollowing
) {
scope.launch {
if (user.isFollowing) {
model.unfollowUser(user.id)
} else {
model.followUser(user.id)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(users.itemCount) { index ->
users[index]?.let { user ->
FollowItem(
avatar = user.avatar,
nickname = user.nickName,
userId = user.id,
isFollowing = user.isFollowing
) {
scope.launch {
if (user.isFollowing) {
model.unfollowUser(user.id)
} else {
model.followUser(user.id)
}
}
}
}
}
}
PullRefreshIndicator(
refreshing = model.isLoading,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}

View File

@@ -21,9 +21,14 @@ import kotlinx.coroutines.launch
object FollowingListViewModel : ViewModel() {
private val userService = UserServiceImpl()
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
var isLoading by mutableStateOf(false)
val usersFlow = _usersFlow.asStateFlow()
private var userId by mutableStateOf<Int?>(null)
fun loadData(id: Int) {
fun loadData(id: Int, force: Boolean = false) {
if (userId == id && !force) {
return
}
isLoading = true
userId = id
viewModelScope.launch {
Pager(
@@ -38,6 +43,7 @@ object FollowingListViewModel : ViewModel() {
_usersFlow.value = it
}
}
isLoading = false
}
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
@@ -62,7 +68,7 @@ object FollowingListViewModel : ViewModel() {
updateIsFollow(userId, false)
}
fun ResetModel(){
fun ResetModel() {
userId = null
}

View File

@@ -23,7 +23,6 @@ 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
@@ -43,7 +42,6 @@ object MyProfileViewModel : ViewModel() {
val profile = accountService.getMyAccountProfile()
MyProfileViewModel.profile = profile
}
fun loadProfile(pullRefresh: Boolean = false) {
if (!firstLoad && !pullRefresh) return
viewModelScope.launch {

View File

@@ -1,483 +0,0 @@
package com.aiosman.riderpro.ui.index.tabs.profile
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
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.PaddingValues
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.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.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
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.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.LocalContext
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.paging.PagingData
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.entity.MomentEntity
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Profile(
onUpdateBanner: ((Uri, Context) -> Unit)? = null,
profile: AccountProfileEntity? = null,
onLogout: () -> Unit = {},
onFollowClick: () -> Unit = {},
onChatClick: () -> Unit = {},
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>(
PagingData.empty()
).asStateFlow(),
isSelf: Boolean = true
) {
Box(
modifier = Modifier.background(Color(0xffFFFFFF))
) {
val userHeight: Int = 187
val bannerHeight = 500
var headerBannerMaxHeight: Int = userHeight + bannerHeight
val headerBannerMinHeight = 100
val speedFactor = 0.75f
var currentHeaderHeight by rememberSaveable { mutableStateOf<Int>(headerBannerMaxHeight) }
var scrollState = rememberLazyListState()
var gridScrollState = rememberLazyStaggeredGridState()
var pagerState = rememberPagerState(pageCount = { 2 })
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
val context = LocalContext.current
var expanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val moments = sharedFlow.collectAsLazyPagingItems()
val pickBannerImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
onUpdateBanner?.invoke(it, context)
}
}
}
val parentScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
val delta = (available.y * speedFactor).toInt()
// 如果是向下滑动,未滑动到列表顶部,则不展开头部
if (pagerState.currentPage == 0) {
if (delta > 0 && (gridScrollState.firstVisibleItemIndex > 0 || gridScrollState.firstVisibleItemScrollOffset > 0)) {
return Offset.Zero
}
}
if (pagerState.currentPage == 1) {
if (delta > 0 && (scrollState.firstVisibleItemIndex > 0 || scrollState.firstVisibleItemScrollOffset > 0)) {
return Offset.Zero
}
}
// 计算新的高度
val newHeight = currentHeaderHeight + delta
val previousHeight = currentHeaderHeight
val newCurrentHeaderHeight =
newHeight.coerceIn(headerBannerMinHeight, headerBannerMaxHeight)
val consumedHeader = newCurrentHeaderHeight - previousHeight
currentHeaderHeight = newCurrentHeaderHeight
return Offset(x = 0f, y = consumedHeader / speedFactor)
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.nestedScroll(parentScrollConnection)
.verticalScroll(
state = rememberScrollState()
)
.background(Color(0xfff8f8f8))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(currentHeaderHeight.dp)
) {
// banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(currentHeaderHeight.dp - userHeight.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 = 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
)
}
}
if (isSelf) {
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
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
expanded = false
scope.launch {
onLogout()
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)
}
}
)
)
}
}
}
Box(
modifier = Modifier.fillMaxWidth()
) {
// user info
Column(
modifier = Modifier
.fillMaxWidth()
.height(if (currentHeaderHeight.dp > bannerHeight.dp) userHeight.dp else currentHeaderHeight.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// 个人信息
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
profile?.let {
UserItem(it)
}
}
Spacer(modifier = Modifier.height(16.dp))
profile?.let {
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
if (isSelf) {
SelfProfileAction {
navController.navigate(NavigationRoute.AccountEdit.route)
}
} else {
OtherProfileAction(
it,
onFollow = {
onFollowClick()
},
onChat = {
onChatClick()
}
)
}
}
}
// collapsed bar
}
val thresholdHeight = 200 // 设置阈值高度
val startChangeHeight = headerBannerMinHeight + thresholdHeight
val alpha = if (currentHeaderHeight < startChangeHeight) {
((currentHeaderHeight - headerBannerMinHeight).toFloat() / thresholdHeight.toFloat()).coerceIn(
0f,
1f
)
} else {
1f // 高度大于阈值时alpha 为 0
}
Column(
modifier = Modifier
.fillMaxWidth()
// 保持在最低高度和当前高度之间
.alpha(1 - alpha)
.background(Color(0xfff8f8f8))
.padding(horizontal = 16.dp)
) {
StatusBarSpacer()
Row(
modifier = Modifier.height(headerBannerMinHeight.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.Black
)
}
Spacer(modifier = Modifier.height(currentHeaderHeight.dp - headerBannerMinHeight.dp))
}
}
}
Spacer(modifier = Modifier.height(16.dp))
UserContentPageIndicator(pagerState)
Spacer(modifier = Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { page ->
when (page) {
0 -> {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(
8.dp
),
verticalItemSpacing = 8.dp,
modifier = Modifier.fillMaxSize(),
state = gridScrollState,
contentPadding = PaddingValues(8.dp)
) {
items(1) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.75f)
.clip(
RoundedCornerShape(8.dp)
)
.background(Color.White)
.padding(8.dp)
.noRippleClickable {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
}
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(
RoundedCornerShape(8.dp)
)
.background(Color(0xfff5f5f5))
) {
Icon(
Icons.Default.Add,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.align(Alignment.Center)
)
}
}
}
items(moments.itemCount) { idx ->
val moment = moments[idx] ?: return@items
GalleryItem(moment)
}
items(2) {
Spacer(modifier = Modifier.height(120.dp))
}
}
}
1 -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = scrollState
) {
if (moments.itemCount == 0) {
item {
EmptyMomentPostUnit()
}
}
item {
for (idx in 0 until moments.itemCount) {
val moment = moments[idx]
moment?.let {
MomentPostUnit(it)
}
}
}
item {
Spacer(modifier = Modifier.height(120.dp))
}
}
}
}
}
}
}
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@@ -136,13 +137,14 @@ fun ProfileV3(
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
toolbarScrollable = true,
enabled = enabled,
toolbar = {
toolbar = { toolbarScrollState ->
Column(
modifier = Modifier
.fillMaxWidth()
// 保持在最低高度和当前高度之间
.background(Color(0xfff8f8f8))
.padding(horizontal = 16.dp)
) {
StatusBarSpacer()
@@ -177,8 +179,7 @@ fun ProfileV3(
.graphicsLayer {
// change alpha of Image as the toolbar expands
alpha = state.toolbarState.progress
},
}.verticalScroll(toolbarScrollState)
) {
Column(
modifier = Modifier

View File

@@ -1,476 +0,0 @@
package com.aiosman.riderpro.ui.index.tabs.profile
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
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.paging.PagingData
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.entity.MomentEntity
import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.pickupAndCompressLauncher
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.File
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ProfileV2(
onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
profile: AccountProfileEntity? = null,
onLogout: () -> Unit = {},
onFollowClick: () -> Unit = {},
onChatClick: () -> Unit = {},
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>(
PagingData.empty()
).asStateFlow(),
isSelf: Boolean = true
) {
Box(
modifier = Modifier.background(Color(0xffFFFFFF))
) {
var parentScrollThreshold by remember { mutableStateOf(0) }
var remainScrollThreshold by remember { mutableStateOf(0) }
val bannerHeight = 500
var scrollState = rememberLazyListState()
var gridScrollState = rememberLazyStaggeredGridState()
var pagerState = rememberPagerState(pageCount = { 2 })
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
val context = LocalContext.current
var expanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val moments = sharedFlow.collectAsLazyPagingItems()
val rootScrollState = rememberScrollState()
val pickBannerImageLauncher = pickupAndCompressLauncher(
context,
scope
) { uri, file ->
onUpdateBanner?.invoke(uri, file, context)
}
val parentScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
Log.d("ProfileV2", "onPreScroll: $available")
val delta = available.y.toInt()
if (delta < 0 && rootScrollState.value < parentScrollThreshold - remainScrollThreshold) {
val scrollAmount = minOf(
-delta,
parentScrollThreshold - remainScrollThreshold - rootScrollState.value
)
scope.launch {
Log.d("ProfileV2", "scrollBy: $scrollAmount")
rootScrollState.scrollBy(scrollAmount.toFloat())
}
return Offset(0f, -scrollAmount.toFloat())
}
return Offset.Zero
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.nestedScroll(parentScrollConnection)
.verticalScroll(
state = rootScrollState
)
.background(Color(0xfff8f8f8))
) {
Box(
modifier = Modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned {
parentScrollThreshold = it.size.height
}
) {
// banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.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 = 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
)
}
}
if (isSelf) {
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
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
width = 250,
menuItems = listOf(
MenuItem(
stringResource(R.string.logout),
R.mipmap.rider_pro_logout
) {
expanded = false
scope.launch {
onLogout()
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)
}
}
)
)
}
}
}
Box(
modifier = Modifier.fillMaxWidth()
) {
// user info
Column(
modifier = Modifier
.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(16.dp))
// 个人信息
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
profile?.let {
UserItem(it)
}
}
Spacer(modifier = Modifier.height(16.dp))
profile?.let {
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
if (isSelf) {
SelfProfileAction {
navController.navigate(NavigationRoute.AccountEdit.route)
}
} else {
OtherProfileAction(
it,
onFollow = {
onFollowClick()
},
onChat = {
onChatClick()
}
)
}
}
}
// collapsed bar
}
}
}
val miniBarAlpha =
if (rootScrollState.value >= parentScrollThreshold - remainScrollThreshold) {
1f
} else {
0f
}
Column(
modifier = Modifier
.fillMaxWidth()
.alpha(miniBarAlpha)
// 保持在最低高度和当前高度之间
.background(Color(0xfff8f8f8))
.padding(horizontal = 16.dp)
.onGloballyPositioned {
remainScrollThreshold = it.size.height
}
.align(Alignment.BottomCenter)
) {
Spacer(modifier = Modifier.height(48.dp))
Row(
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.Black
)
}
}
}
// 页面指示器
Spacer(modifier = Modifier.height(16.dp))
UserContentPageIndicator(pagerState)
Spacer(modifier = Modifier.height(16.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.height(900.dp)
) { page ->
when (page) {
0 -> {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(
8.dp
),
verticalItemSpacing = 8.dp,
modifier = Modifier.fillMaxSize(),
state = gridScrollState,
contentPadding = PaddingValues(8.dp)
) {
items(1) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.75f)
.clip(
RoundedCornerShape(8.dp)
)
.background(Color.White)
.padding(8.dp)
.noRippleClickable {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
}
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(
RoundedCornerShape(8.dp)
)
.background(Color(0xfff5f5f5))
) {
Icon(
Icons.Default.Add,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.align(Alignment.Center)
)
}
}
}
items(moments.itemCount) { idx ->
val moment = moments[idx] ?: return@items
GalleryItem(moment)
}
items(2) {
Spacer(modifier = Modifier.height(120.dp))
}
}
}
1 -> {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = scrollState
) {
if (moments.itemCount == 0) {
item {
EmptyMomentPostUnit()
}
}
item {
for (idx in 0 until moments.itemCount) {
val moment = moments[idx]
moment?.let {
MomentPostUnit(it)
}
}
}
item {
Spacer(modifier = Modifier.height(120.dp))
}
}
}
}
}
}
}
}

View File

@@ -125,7 +125,7 @@ fun UserAuthScreen() {
loadLoginCaptcha()
Toast.makeText(
context,
"incorrect captcha,please try again",
context.getString(R.string.incorrect_captcha_please_try_again),
Toast.LENGTH_SHORT
).show()
} else {
@@ -197,8 +197,7 @@ fun UserAuthScreen() {
onLogin(captchaInfo)
}
}
}
},
)
}
Column(

View File

@@ -93,7 +93,7 @@ object NewPostViewModel : ViewModel() {
}
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
// 刷新个人动态
MyProfileViewModel.loadProfile()
MyProfileViewModel.loadProfile(pullRefresh = true)
MomentViewModel.refreshPager()
}

View File

@@ -114,6 +114,7 @@ class PostViewModel(
moment?.let {
userService.followUser(it.authorId.toString())
moment = moment?.copy(followStatus = true)
// 更新我的关注页面的关注数
}
}
@@ -121,6 +122,7 @@ class PostViewModel(
moment?.let {
userService.unFollowUser(it.authorId.toString())
moment = moment?.copy(followStatus = false)
// 更新我的关注页面的关注数
}
}

View File

@@ -5,8 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.exp.viewModelFactory
import com.aiosman.riderpro.ui.index.tabs.profile.Profile
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV2
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV3
import com.aiosman.riderpro.ui.navigateToChat

View File

@@ -82,4 +82,8 @@
<string name="resend">重新发送 %s</string>
<string name="error_40002_user_not_exist">用户不存在</string>
<string name="captcha_hint">请依次点击图片中的元素</string>
<string name="captcha">验证码</string>
<string name="refresh">刷新</string>
<string name="clear">清除</string>
<string name="incorrect_captcha_please_try_again">验证码错误,请重试</string>
</resources>

View File

@@ -81,4 +81,8 @@
<string name="resend">Resend in %s</string>
<string name="error_40002_user_not_exist">user not exist</string>
<string name="captcha_hint">Please click on the dots in the image in the correct order.</string>
<string name="captcha">Chaptcha</string>
<string name="refresh">Refresh</string>
<string name="clear">Clear</string>
<string name="incorrect_captcha_please_try_again">incorrect captcha,please try again</string>
</resources>