更新代码
This commit is contained in:
@@ -154,7 +154,7 @@ fun ClickCaptchaDialog(
|
|||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Text("Captcha")
|
Text(stringResource(R.string.captcha))
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
@@ -163,14 +163,14 @@ fun ClickCaptchaDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
ClickCaptchaView(
|
ClickCaptchaView(
|
||||||
captchaData = captchaData!!,
|
captchaData = captchaData,
|
||||||
onPositionClicked = onPositionClicked
|
onPositionClicked = onPositionClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Refresh",
|
text = stringResource(R.string.refresh),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ fun CollapsingToolbarScaffold(
|
|||||||
toolbarModifier: Modifier = Modifier,
|
toolbarModifier: Modifier = Modifier,
|
||||||
toolbarClipToBounds: Boolean = true,
|
toolbarClipToBounds: Boolean = true,
|
||||||
toolbarScrollable: Boolean = false,
|
toolbarScrollable: Boolean = false,
|
||||||
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
|
toolbar: @Composable CollapsingToolbarScope.(ScrollState) -> Unit,
|
||||||
body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
|
body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val flingBehavior = ScrollableDefaults.flingBehavior()
|
val flingBehavior = ScrollableDefaults.flingBehavior()
|
||||||
@@ -122,7 +122,7 @@ fun CollapsingToolbarScaffold(
|
|||||||
toolbarState,
|
toolbarState,
|
||||||
toolbarScrollState
|
toolbarScrollState
|
||||||
)
|
)
|
||||||
toolbar()
|
toolbar(toolbarScrollState)
|
||||||
}
|
}
|
||||||
|
|
||||||
CollapsingToolbarScaffoldScopeInstance.body()
|
CollapsingToolbarScaffoldScopeInstance.body()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.aiosman.riderpro.ui.composables.toolbar
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.SubcomposeLayout
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
@@ -14,7 +15,7 @@ fun ToolbarWithFabScaffold(
|
|||||||
scrollStrategy: ScrollStrategy,
|
scrollStrategy: ScrollStrategy,
|
||||||
toolbarModifier: Modifier = Modifier,
|
toolbarModifier: Modifier = Modifier,
|
||||||
toolbarClipToBounds: Boolean = true,
|
toolbarClipToBounds: Boolean = true,
|
||||||
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
|
toolbar: @Composable CollapsingToolbarScope.(ScrollState) -> Unit,
|
||||||
toolbarScrollable: Boolean = false,
|
toolbarScrollable: Boolean = false,
|
||||||
fab: @Composable () -> Unit,
|
fab: @Composable () -> Unit,
|
||||||
fabPosition: FabPosition = FabPosition.End,
|
fabPosition: FabPosition = FabPosition.End,
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package com.aiosman.riderpro.ui.follower
|
package com.aiosman.riderpro.ui.follower
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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 com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowerListScreen(userId: Int) {
|
fun FollowerListScreen(userId: Int) {
|
||||||
val model = FollowerListViewModel
|
val model = FollowerListViewModel
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val refreshState = rememberPullRefreshState(model.isLoading, onRefresh = {
|
||||||
|
model.loadData(userId, true)
|
||||||
|
})
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.loadData(userId)
|
model.loadData(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
var dataFlow = model.usersFlow
|
var dataFlow = model.usersFlow
|
||||||
var users = dataFlow.collectAsLazyPagingItems()
|
var users = dataFlow.collectAsLazyPagingItems()
|
||||||
@@ -35,27 +48,39 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
|
||||||
}
|
}
|
||||||
LazyColumn(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.pullRefresh(refreshState)
|
||||||
) {
|
) {
|
||||||
items(users.itemCount) { index ->
|
LazyColumn(
|
||||||
users[index]?.let { user ->
|
modifier = Modifier.fillMaxSize()
|
||||||
FollowItem(
|
) {
|
||||||
avatar = user.avatar,
|
items(users.itemCount) { index ->
|
||||||
nickname = user.nickName,
|
users[index]?.let { user ->
|
||||||
userId = user.id,
|
FollowItem(
|
||||||
isFollowing = user.isFollowing
|
avatar = user.avatar,
|
||||||
) {
|
nickname = user.nickName,
|
||||||
scope.launch {
|
userId = user.id,
|
||||||
if (user.isFollowing) {
|
isFollowing = user.isFollowing
|
||||||
model.unFollowUser(user.id)
|
) {
|
||||||
} else {
|
scope.launch {
|
||||||
model.followUser(user.id)
|
if (user.isFollowing) {
|
||||||
|
model.unFollowUser(user.id)
|
||||||
|
} else {
|
||||||
|
model.followUser(user.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = model.isLoading,
|
||||||
|
state = refreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,10 +23,12 @@ object FollowerListViewModel : ViewModel() {
|
|||||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||||
val usersFlow = _usersFlow.asStateFlow()
|
val usersFlow = _usersFlow.asStateFlow()
|
||||||
private var userId by mutableStateOf<Int?>(null)
|
private var userId by mutableStateOf<Int?>(null)
|
||||||
fun loadData(id: Int) {
|
var isLoading by mutableStateOf(false)
|
||||||
if (userId == id) {
|
fun loadData(id: Int,force : Boolean = false) {
|
||||||
|
if (userId == id && !force) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
isLoading = true
|
||||||
userId = id
|
userId = id
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Pager(
|
Pager(
|
||||||
@@ -41,6 +43,7 @@ object FollowerListViewModel : ViewModel() {
|
|||||||
_usersFlow.value = it
|
_usersFlow.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
|
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package com.aiosman.riderpro.ui.follower
|
package com.aiosman.riderpro.ui.follower
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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 com.aiosman.riderpro.ui.composables.StatusBarMaskLayout
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowingListScreen(userId: Int) {
|
fun FollowingListScreen(userId: Int) {
|
||||||
val model = FollowingListViewModel
|
val model = FollowingListViewModel
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val refreshState = rememberPullRefreshState(model.isLoading, onRefresh = {
|
||||||
|
model.loadData(userId, true)
|
||||||
|
})
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.loadData(userId)
|
model.loadData(userId)
|
||||||
}
|
}
|
||||||
@@ -35,27 +45,39 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
) {
|
) {
|
||||||
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
|
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
|
||||||
}
|
}
|
||||||
LazyColumn(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.pullRefresh(refreshState)
|
||||||
) {
|
) {
|
||||||
items(users.itemCount) { index ->
|
LazyColumn(
|
||||||
users[index]?.let { user ->
|
modifier = Modifier.fillMaxSize()
|
||||||
FollowItem(
|
) {
|
||||||
avatar = user.avatar,
|
items(users.itemCount) { index ->
|
||||||
nickname = user.nickName,
|
users[index]?.let { user ->
|
||||||
userId = user.id,
|
FollowItem(
|
||||||
isFollowing = user.isFollowing
|
avatar = user.avatar,
|
||||||
) {
|
nickname = user.nickName,
|
||||||
scope.launch {
|
userId = user.id,
|
||||||
if (user.isFollowing) {
|
isFollowing = user.isFollowing
|
||||||
model.unfollowUser(user.id)
|
) {
|
||||||
} else {
|
scope.launch {
|
||||||
model.followUser(user.id)
|
if (user.isFollowing) {
|
||||||
|
model.unfollowUser(user.id)
|
||||||
|
} else {
|
||||||
|
model.followUser(user.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = model.isLoading,
|
||||||
|
state = refreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,14 @@ import kotlinx.coroutines.launch
|
|||||||
object FollowingListViewModel : ViewModel() {
|
object FollowingListViewModel : ViewModel() {
|
||||||
private val userService = UserServiceImpl()
|
private val userService = UserServiceImpl()
|
||||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
val usersFlow = _usersFlow.asStateFlow()
|
val usersFlow = _usersFlow.asStateFlow()
|
||||||
private var userId by mutableStateOf<Int?>(null)
|
private var userId by mutableStateOf<Int?>(null)
|
||||||
fun loadData(id: Int) {
|
fun loadData(id: Int, force: Boolean = false) {
|
||||||
|
if (userId == id && !force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading = true
|
||||||
userId = id
|
userId = id
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Pager(
|
Pager(
|
||||||
@@ -38,6 +43,7 @@ object FollowingListViewModel : ViewModel() {
|
|||||||
_usersFlow.value = it
|
_usersFlow.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
|
private fun updateIsFollow(id: Int, isFollow: Boolean = true) {
|
||||||
@@ -62,7 +68,7 @@ object FollowingListViewModel : ViewModel() {
|
|||||||
updateIsFollow(userId, false)
|
updateIsFollow(userId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ResetModel(){
|
fun ResetModel() {
|
||||||
userId = null
|
userId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import com.aiosman.riderpro.entity.MomentEntity
|
|||||||
import com.aiosman.riderpro.entity.MomentPagingSource
|
import com.aiosman.riderpro.entity.MomentPagingSource
|
||||||
import com.aiosman.riderpro.entity.MomentRemoteDataSource
|
import com.aiosman.riderpro.entity.MomentRemoteDataSource
|
||||||
import com.aiosman.riderpro.entity.MomentServiceImpl
|
import com.aiosman.riderpro.entity.MomentServiceImpl
|
||||||
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -43,7 +42,6 @@ object MyProfileViewModel : ViewModel() {
|
|||||||
val profile = accountService.getMyAccountProfile()
|
val profile = accountService.getMyAccountProfile()
|
||||||
MyProfileViewModel.profile = profile
|
MyProfileViewModel.profile = profile
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadProfile(pullRefresh: Boolean = false) {
|
fun loadProfile(pullRefresh: Boolean = false) {
|
||||||
if (!firstLoad && !pullRefresh) return
|
if (!firstLoad && !pullRefresh) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ import androidx.compose.foundation.pager.HorizontalPager
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
@@ -136,13 +137,14 @@ fun ProfileV3(
|
|||||||
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
|
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
|
||||||
toolbarScrollable = true,
|
toolbarScrollable = true,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
toolbar = {
|
toolbar = { toolbarScrollState ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
// 保持在最低高度和当前高度之间
|
// 保持在最低高度和当前高度之间
|
||||||
.background(Color(0xfff8f8f8))
|
.background(Color(0xfff8f8f8))
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
|
|
||||||
@@ -177,8 +179,7 @@ fun ProfileV3(
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
// change alpha of Image as the toolbar expands
|
// change alpha of Image as the toolbar expands
|
||||||
alpha = state.toolbarState.progress
|
alpha = state.toolbarState.progress
|
||||||
},
|
}.verticalScroll(toolbarScrollState)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ fun UserAuthScreen() {
|
|||||||
loadLoginCaptcha()
|
loadLoginCaptcha()
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"incorrect captcha,please try again",
|
context.getString(R.string.incorrect_captcha_please_try_again),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
} else {
|
} else {
|
||||||
@@ -197,8 +197,7 @@ fun UserAuthScreen() {
|
|||||||
onLogin(captchaInfo)
|
onLogin(captchaInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ object NewPostViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
|
momentService.createMoment(textContent, 1, uploadImageList, relPostId)
|
||||||
// 刷新个人动态
|
// 刷新个人动态
|
||||||
MyProfileViewModel.loadProfile()
|
MyProfileViewModel.loadProfile(pullRefresh = true)
|
||||||
MomentViewModel.refreshPager()
|
MomentViewModel.refreshPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class PostViewModel(
|
|||||||
moment?.let {
|
moment?.let {
|
||||||
userService.followUser(it.authorId.toString())
|
userService.followUser(it.authorId.toString())
|
||||||
moment = moment?.copy(followStatus = true)
|
moment = moment?.copy(followStatus = true)
|
||||||
|
// 更新我的关注页面的关注数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ class PostViewModel(
|
|||||||
moment?.let {
|
moment?.let {
|
||||||
userService.unFollowUser(it.authorId.toString())
|
userService.unFollowUser(it.authorId.toString())
|
||||||
moment = moment?.copy(followStatus = false)
|
moment = moment?.copy(followStatus = false)
|
||||||
|
// 更新我的关注页面的关注数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.exp.viewModelFactory
|
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.index.tabs.profile.ProfileV3
|
||||||
import com.aiosman.riderpro.ui.navigateToChat
|
import com.aiosman.riderpro.ui.navigateToChat
|
||||||
|
|
||||||
|
|||||||
@@ -82,4 +82,8 @@
|
|||||||
<string name="resend">重新发送 %s</string>
|
<string name="resend">重新发送 %s</string>
|
||||||
<string name="error_40002_user_not_exist">用户不存在</string>
|
<string name="error_40002_user_not_exist">用户不存在</string>
|
||||||
<string name="captcha_hint">请依次点击图片中的元素</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>
|
</resources>
|
||||||
@@ -81,4 +81,8 @@
|
|||||||
<string name="resend">Resend in %s</string>
|
<string name="resend">Resend in %s</string>
|
||||||
<string name="error_40002_user_not_exist">user not exist</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_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>
|
</resources>
|
||||||
Reference in New Issue
Block a user