改包名com.aiosman.ravenow
This commit is contained in:
231
app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt
Normal file
231
app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt
Normal file
@@ -0,0 +1,231 @@
|
||||
package com.aiosman.ravenow.ui.index
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemColors
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.index.tabs.add.AddPage
|
||||
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.MomentsList
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.ProfileWrap
|
||||
import com.aiosman.ravenow.ui.index.tabs.search.DiscoverScreen
|
||||
import com.aiosman.ravenow.ui.index.tabs.shorts.ShortVideo
|
||||
import com.aiosman.ravenow.ui.index.tabs.street.StreetPage
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun IndexScreen() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val model = IndexViewModel
|
||||
val navigationBarHeight = with(LocalDensity.current) {
|
||||
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||
}
|
||||
val navController = LocalNavController.current
|
||||
val item = listOf(
|
||||
NavigationItem.Home,
|
||||
NavigationItem.Search,
|
||||
NavigationItem.Add,
|
||||
NavigationItem.Notification,
|
||||
NavigationItem.Profile
|
||||
)
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val pagerState = rememberPagerState(pageCount = { item.size })
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||
}
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
modifier = Modifier.height(56.dp + navigationBarHeight),
|
||||
containerColor = Color.Black
|
||||
) {
|
||||
item.forEachIndexed { idx, it ->
|
||||
val isSelected = model.tabIndex == idx
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) Color.White else Color.White,
|
||||
animationSpec = tween(durationMillis = 250), label = ""
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (it.route === NavigationItem.Add.route) {
|
||||
NewPostViewModel.asNewPost()
|
||||
navController.navigate(NavigationRoute.NewPost.route)
|
||||
return@NavigationBarItem
|
||||
}
|
||||
coroutineScope.launch {
|
||||
pagerState.scrollToPage(idx)
|
||||
}
|
||||
model.tabIndex = idx
|
||||
},
|
||||
colors = NavigationBarItemColors(
|
||||
selectedTextColor = Color.Red,
|
||||
selectedIndicatorColor = Color.Black,
|
||||
unselectedTextColor = Color.Red,
|
||||
disabledIconColor = Color.Red,
|
||||
disabledTextColor = Color.Red,
|
||||
selectedIconColor = iconTint,
|
||||
unselectedIconColor = iconTint,
|
||||
),
|
||||
icon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = if (isSelected) it.selectedIcon() else it.icon(),
|
||||
contentDescription = null,
|
||||
tint = iconTint
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
innerPadding
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.background(AppColors.background).padding(0.dp),
|
||||
beyondBoundsPageCount = 5,
|
||||
userScrollEnabled = false
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> Home()
|
||||
1 -> DiscoverScreen()
|
||||
2 -> Add()
|
||||
3 -> Notifications()
|
||||
4 -> Profile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Home() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
MomentsList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun Street() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StreetPage()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Add() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Black, darkIcons = !AppState.darkMode)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AddPage()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Video() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Black, darkIcons = false)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ShortVideo()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun Profile() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ProfileWrap()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Notifications() {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
NotificationsScreen()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.aiosman.ravenow.ui.index
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
object IndexViewModel:ViewModel() {
|
||||
var tabIndex by mutableStateOf(0)
|
||||
|
||||
fun ResetModel(){
|
||||
tabIndex = 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.aiosman.ravenow.ui.index
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.aiosman.ravenow.R
|
||||
|
||||
sealed class NavigationItem(
|
||||
val route: String,
|
||||
val icon: @Composable () -> ImageVector,
|
||||
val selectedIcon: @Composable () -> ImageVector = icon
|
||||
) {
|
||||
data object Home : NavigationItem("Home",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home_hl) }
|
||||
)
|
||||
|
||||
data object Street : NavigationItem("Street",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_location) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_location_filed) }
|
||||
)
|
||||
|
||||
data object Add : NavigationItem("Add",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_post_hl) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_post_hl) }
|
||||
)
|
||||
|
||||
data object Message : NavigationItem("Message",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_video_outline) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_video) }
|
||||
)
|
||||
|
||||
data object Notification : NavigationItem("Notification",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification)},
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification_hl) }
|
||||
)
|
||||
|
||||
data object Profile : NavigationItem("Profile",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile_hl) }
|
||||
)
|
||||
|
||||
data object Search : NavigationItem("Search",
|
||||
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search) },
|
||||
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search_hl) }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.add
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||
import com.aiosman.ravenow.R
|
||||
|
||||
@Composable
|
||||
fun AddPage(){
|
||||
val navController = LocalNavController.current
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)) {
|
||||
AddBtn(icon = R.drawable.rider_pro_icon_rider_share, text = "Rave NowShare") {
|
||||
NewPostViewModel.asNewPost()
|
||||
navController.navigate("NewPost")
|
||||
}
|
||||
// AddBtn(icon = R.drawable.rider_pro_location_create, text = "Location Create")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddBtn(@DrawableRes icon: Int, text: String,onClick: (() -> Unit)? = {}){
|
||||
Row (modifier = Modifier
|
||||
.fillMaxWidth().padding(24.dp).clickable {
|
||||
onClick?.invoke()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically){
|
||||
Image(
|
||||
modifier = Modifier.size(40.dp),
|
||||
painter = painterResource(id = icon), contentDescription = null)
|
||||
Text(modifier = Modifier.padding(start = 24.dp),text = text, color = Color.White,fontSize = 22.sp, style = TextStyle(fontWeight = FontWeight.Bold))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.message
|
||||
|
||||
import android.widget.Toast
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
|
||||
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* 消息列表界面
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun NotificationsScreen() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val context = LocalContext.current
|
||||
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
|
||||
MessageListViewModel.viewModelScope.launch {
|
||||
MessageListViewModel.initData(context, force = true)
|
||||
}
|
||||
})
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||
MessageListViewModel.initData(context)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.pullRefresh(state)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
NotificationIndicator(
|
||||
MessageListViewModel.likeNoticeCount,
|
||||
R.drawable.rider_pro_moment_like,
|
||||
stringResource(R.string.like_upper)
|
||||
) {
|
||||
if (MessageListViewModel.likeNoticeCount > 0) {
|
||||
// 刷新点赞消息列表
|
||||
LikeNoticeViewModel.isFirstLoad = true
|
||||
// 清除点赞消息数量
|
||||
MessageListViewModel.clearLikeNoticeCount()
|
||||
}
|
||||
navController.navigate(NavigationRoute.Likes.route)
|
||||
}
|
||||
NotificationIndicator(
|
||||
MessageListViewModel.followNoticeCount,
|
||||
R.drawable.rider_pro_followers,
|
||||
stringResource(R.string.followers_upper)
|
||||
) {
|
||||
if (MessageListViewModel.followNoticeCount > 0) {
|
||||
// 刷新关注消息列表
|
||||
FollowerNoticeViewModel.isFirstLoad = true
|
||||
MessageListViewModel.clearFollowNoticeCount()
|
||||
}
|
||||
navController.navigate(NavigationRoute.Followers.route)
|
||||
}
|
||||
NotificationIndicator(
|
||||
MessageListViewModel.commentNoticeCount,
|
||||
R.drawable.rider_pro_comment,
|
||||
stringResource(R.string.comment).uppercase()
|
||||
) {
|
||||
navController.navigate(NavigationRoute.CommentNoticeScreen.route)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = AppColors.divider, modifier = Modifier.padding(16.dp))
|
||||
// NotificationCounterItem(MessageListViewModel.unReadConversationCount.toInt())
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ChatMessageList(
|
||||
MessageListViewModel.chatList,
|
||||
onUserAvatarClick = { conv ->
|
||||
MessageListViewModel.goToUserDetail(conv, navController)
|
||||
},
|
||||
) { conv ->
|
||||
MessageListViewModel.goToChat(conv, navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
MessageListViewModel.isLoading,
|
||||
state,
|
||||
Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationIndicator(
|
||||
notificationCount: Int,
|
||||
iconRes: Int,
|
||||
label: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.TopCenter)
|
||||
.noRippleClickable {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
if (notificationCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(AppColors.main, RoundedCornerShape(16.dp))
|
||||
.padding(4.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Text(
|
||||
text = notificationCount.toString(),
|
||||
color = AppColors.mainText,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
Text(label, modifier = Modifier.align(Alignment.Center), color = AppColors.text)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationCounterItem(count: Int) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
var clickCount by remember { mutableStateOf(0) }
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 16.dp, horizontal = 32.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_notification),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(24.dp).noRippleClickable {
|
||||
clickCount++
|
||||
if (clickCount > 5) {
|
||||
clickCount = 0
|
||||
AppStore.saveDarkMode(!AppState.darkMode)
|
||||
Toast.makeText(context, "Dark mode: ${AppState.darkMode},please restart app", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Text(stringResource(R.string.notifications_upper), fontSize = 18.sp, color = AppColors.text)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (count > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(AppColors.main, RoundedCornerShape(16.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
color = AppColors.mainText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ChatMessageList(
|
||||
items: List<Conversation>,
|
||||
onUserAvatarClick: (Conversation) -> Unit = {},
|
||||
onChatClick: (Conversation) -> Unit = {}
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(items.size) { index ->
|
||||
val item = items[index]
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||
) {
|
||||
Box {
|
||||
CustomAsyncImage(
|
||||
context = LocalContext.current,
|
||||
imageUrl = item.avatar,
|
||||
contentDescription = item.nickname,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(48.dp))
|
||||
.noRippleClickable {
|
||||
onUserAvatarClick(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp)
|
||||
.noRippleClickable {
|
||||
onChatClick(item)
|
||||
}
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = item.nickname,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = item.lastMessageTime,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = "${if (item.isSelf) "Me: " else ""}${item.displayText}",
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier.weight(1f),
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
if (item.unreadCount > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(AppColors.main, CircleShape)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.unreadCount.toString(),
|
||||
color = AppColors.mainText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.message
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.util.Calendar
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.data.AccountNotice
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.UserService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.exp.formatChatTime
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.navigateToChat
|
||||
import com.aiosman.ravenow.utils.TrtcHelper
|
||||
import com.tencent.imsdk.v2.V2TIMConversation
|
||||
import com.tencent.imsdk.v2.V2TIMConversationResult
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMMessage
|
||||
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class Conversation(
|
||||
val id: String,
|
||||
val trtcUserId: String,
|
||||
val nickname: String,
|
||||
val lastMessage: String,
|
||||
val lastMessageTime: String,
|
||||
val avatar: String = "",
|
||||
val unreadCount: Int = 0,
|
||||
val displayText: String,
|
||||
val isSelf: Boolean
|
||||
) {
|
||||
companion object {
|
||||
fun convertToConversation(msg: V2TIMConversation, context: Context): Conversation {
|
||||
val lastMessage = Calendar.getInstance().apply {
|
||||
timeInMillis = msg.lastMessage?.timestamp ?: 0
|
||||
timeInMillis *= 1000
|
||||
}
|
||||
var displayText = ""
|
||||
when (msg.lastMessage?.elemType) {
|
||||
V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {
|
||||
displayText = msg.lastMessage?.textElem?.text ?: ""
|
||||
}
|
||||
|
||||
V2TIMMessage.V2TIM_ELEM_TYPE_IMAGE -> {
|
||||
displayText = "[图片]"
|
||||
}
|
||||
}
|
||||
return Conversation(
|
||||
id = msg.conversationID,
|
||||
nickname = msg.showName,
|
||||
lastMessage = msg.lastMessage?.textElem?.text ?: "",
|
||||
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||
avatar = msg.faceUrl,
|
||||
unreadCount = msg.unreadCount,
|
||||
trtcUserId = msg.userID,
|
||||
displayText = displayText,
|
||||
isSelf = msg.lastMessage.sender == AppState.profile?.trtcUserId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MessageListViewModel : ViewModel() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
val userService: UserService = UserServiceImpl()
|
||||
var noticeInfo by mutableStateOf<AccountNotice?>(null)
|
||||
var chatList by mutableStateOf<List<Conversation>>(emptyList())
|
||||
private val _commentItemsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
||||
var isLoading by mutableStateOf(false)
|
||||
var unReadConversationCount by mutableStateOf(0L)
|
||||
var isFirstLoad = true
|
||||
suspend fun initData(context: Context, force: Boolean = false) {
|
||||
loadChatList(context)
|
||||
loadUnreadCount()
|
||||
if (!isFirstLoad && !force) {
|
||||
return
|
||||
}
|
||||
if (force) {
|
||||
isLoading = true
|
||||
}
|
||||
isFirstLoad = false
|
||||
val info = accountService.getMyNoticeInfo()
|
||||
noticeInfo = info
|
||||
|
||||
isLoading = false
|
||||
|
||||
}
|
||||
|
||||
val likeNoticeCount
|
||||
get() = noticeInfo?.likeCount ?: 0
|
||||
val followNoticeCount
|
||||
get() = noticeInfo?.followCount ?: 0
|
||||
val favouriteNoticeCount
|
||||
get() = noticeInfo?.favoriteCount ?: 0
|
||||
val commentNoticeCount
|
||||
get() = noticeInfo?.commentCount ?: 0
|
||||
|
||||
private fun updateIsRead(id: Int) {
|
||||
val currentPagingData = _commentItemsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { commentEntity ->
|
||||
if (commentEntity.id == id) {
|
||||
commentEntity.copy(unread = false)
|
||||
} else {
|
||||
commentEntity
|
||||
}
|
||||
}
|
||||
_commentItemsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
|
||||
fun clearLikeNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(likeCount = 0)
|
||||
}
|
||||
|
||||
fun clearFollowNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(followCount = 0)
|
||||
}
|
||||
|
||||
fun clearFavouriteNoticeCount() {
|
||||
noticeInfo = noticeInfo?.copy(favoriteCount = 0)
|
||||
}
|
||||
|
||||
fun updateUnReadCount(delta: Int) {
|
||||
noticeInfo?.let {
|
||||
noticeInfo = it.copy(commentCount = it.commentCount + delta)
|
||||
}
|
||||
}
|
||||
|
||||
fun ResetModel() {
|
||||
_commentItemsFlow.value = PagingData.empty()
|
||||
noticeInfo = null
|
||||
isLoading = false
|
||||
isFirstLoad = true
|
||||
}
|
||||
|
||||
suspend fun loadChatList(context: Context) {
|
||||
val result = suspendCoroutine { continuation ->
|
||||
V2TIMManager.getConversationManager().getConversationList(
|
||||
0,
|
||||
Int.MAX_VALUE,
|
||||
object : V2TIMValueCallback<V2TIMConversationResult> {
|
||||
override fun onSuccess(t: V2TIMConversationResult?) {
|
||||
continuation.resumeWith(Result.success(t))
|
||||
}
|
||||
|
||||
override fun onError(code: Int, desc: String?) {
|
||||
continuation.resumeWith(Result.failure(Exception("Error $code: $desc")))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
chatList = result?.conversationList?.map { msg: V2TIMConversation ->
|
||||
Conversation.convertToConversation(msg, context)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun loadUnreadCount() {
|
||||
try {
|
||||
this.unReadConversationCount = TrtcHelper.loadUnreadCount()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
this.unReadConversationCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun goToChat(
|
||||
conversation: Conversation,
|
||||
navController: NavHostController
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
|
||||
navController.navigateToChat(profile.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun goToUserDetail(
|
||||
conversation: Conversation,
|
||||
navController: NavController
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val profile = userService.getUserProfileByTrtcUserId(conversation.trtcUserId)
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
profile.id.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshConversation(context: Context, userId: String) {
|
||||
viewModelScope.launch {
|
||||
loadChatList(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreMomentsList
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 动态列表
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MomentsList() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val model = MomentViewModel
|
||||
val navigationBarPaddings =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
var pagerState = rememberPagerState { 2 }
|
||||
var scope = rememberCoroutineScope()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = statusBarPaddingValues.calculateTopPadding(),
|
||||
bottom = navigationBarPaddings
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().height(44.dp),
|
||||
// center the tabs
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(0)
|
||||
}
|
||||
},
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
) {
|
||||
Text(text = "Worldwide", fontSize = 16.sp, color = AppColors.text,fontWeight = FontWeight.W600)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(if (pagerState.currentPage == 0) AppColors.text else AppColors.background)
|
||||
)
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(1)
|
||||
}
|
||||
},
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "Following", fontSize = 16.sp, color = AppColors.text, fontWeight = FontWeight.W600)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(if (pagerState.currentPage == 1) AppColors.text else AppColors.background)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
when (it) {
|
||||
0 -> {
|
||||
ExploreMomentsList()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
TimelineMomentsList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
|
||||
object MomentViewModel : ViewModel() {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 动态列表
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ExploreMomentsList() {
|
||||
val model = MomentExploreViewModel
|
||||
var dataFlow = model.momentsFlow
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||
model.refreshPager(
|
||||
pullRefresh = true
|
||||
)
|
||||
})
|
||||
LaunchedEffect(Unit) {
|
||||
model.refreshPager()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
Box(Modifier.pullRefresh(state)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(
|
||||
moments.itemCount,
|
||||
key = { idx -> idx }
|
||||
) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
MomentCard(momentEntity = momentItem,
|
||||
onAddComment = {
|
||||
scope.launch {
|
||||
model.onAddComment(momentItem.id)
|
||||
}
|
||||
},
|
||||
onLikeClick = {
|
||||
scope.launch {
|
||||
if (momentItem.liked) {
|
||||
model.dislikeMoment(momentItem.id)
|
||||
} else {
|
||||
model.likeMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = {
|
||||
scope.launch {
|
||||
if (momentItem.isFavorite) {
|
||||
model.unfavoriteMoment(momentItem.id)
|
||||
} else {
|
||||
model.favoriteMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFollowClick = {
|
||||
model.followAction(momentItem)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.map
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
object MomentExploreViewModel : ViewModel() {
|
||||
private val momentService: MomentService = MomentServiceImpl()
|
||||
private val userService = UserServiceImpl()
|
||||
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
val momentsFlow = _momentsFlow.asStateFlow()
|
||||
var existsMoment = mutableStateOf(false)
|
||||
var refreshing by mutableStateOf(false)
|
||||
var isFirstLoad = true
|
||||
fun refreshPager(pullRefresh: Boolean = false) {
|
||||
if (!isFirstLoad && !pullRefresh) {
|
||||
return
|
||||
}
|
||||
isFirstLoad = false
|
||||
viewModelScope.launch {
|
||||
if (pullRefresh) {
|
||||
refreshing = true
|
||||
}
|
||||
// 检查是否有动态
|
||||
val existMoments =
|
||||
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
|
||||
if (existMoments.list.isEmpty()) {
|
||||
existsMoment.value = true
|
||||
}
|
||||
if (pullRefresh) {
|
||||
refreshing = false
|
||||
}
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
// 如果没有动态,则显示热门动态
|
||||
explore = true
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_momentsFlow.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLikeCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun likeMoment(id: Int) {
|
||||
momentService.likeMoment(id)
|
||||
updateLikeCount(id)
|
||||
}
|
||||
|
||||
fun updateCommentCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(commentCount = momentItem.commentCount + 1)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun onAddComment(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
updateCommentCount(id)
|
||||
}
|
||||
|
||||
|
||||
fun updateDislikeMomentById(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun dislikeMoment(id: Int) {
|
||||
momentService.dislikeMoment(id)
|
||||
updateDislikeMomentById(id)
|
||||
}
|
||||
|
||||
fun updateFavoriteCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun favoriteMoment(id: Int) {
|
||||
momentService.favoriteMoment(id)
|
||||
updateFavoriteCount(id)
|
||||
}
|
||||
|
||||
fun updateUnfavoriteCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun unfavoriteMoment(id: Int) {
|
||||
momentService.unfavoriteMoment(id)
|
||||
updateUnfavoriteCount(id)
|
||||
}
|
||||
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.authorId == authorId) {
|
||||
momentItem.copy(followStatus = isFollow)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
fun followAction(moment: MomentEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
if (moment.followStatus) {
|
||||
userService.unFollowUser(moment.authorId.toString())
|
||||
} else {
|
||||
userService.followUser(moment.authorId.toString())
|
||||
}
|
||||
updateFollowStatus(moment.authorId, !moment.followStatus)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ResetModel() {
|
||||
_momentsFlow.value = PagingData.empty()
|
||||
isFirstLoad = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 动态列表
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun TimelineMomentsList() {
|
||||
val model = TimelineMomentViewModel
|
||||
var dataFlow = model.momentsFlow
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
val scope = rememberCoroutineScope()
|
||||
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||
model.refreshPager(
|
||||
pullRefresh = true
|
||||
)
|
||||
})
|
||||
LaunchedEffect(Unit) {
|
||||
model.refreshPager()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Box(Modifier.pullRefresh(state)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(
|
||||
moments.itemCount,
|
||||
key = { idx -> moments[idx]?.id ?: idx }
|
||||
) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
MomentCard(momentEntity = momentItem,
|
||||
onAddComment = {
|
||||
scope.launch {
|
||||
model.onAddComment(momentItem.id)
|
||||
}
|
||||
},
|
||||
onLikeClick = {
|
||||
scope.launch {
|
||||
if (momentItem.liked) {
|
||||
model.dislikeMoment(momentItem.id)
|
||||
} else {
|
||||
model.likeMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = {
|
||||
scope.launch {
|
||||
if (momentItem.isFavorite) {
|
||||
model.unfavoriteMoment(momentItem.id)
|
||||
} else {
|
||||
model.favoriteMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFollowClick = {
|
||||
model.followAction(momentItem)
|
||||
},
|
||||
showFollowButton = false
|
||||
)
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .height(4.dp)
|
||||
// .fillMaxWidth()
|
||||
// .background(Color(0xFFF0F2F5))
|
||||
// )
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.filter
|
||||
import androidx.paging.map
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.data.UserService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
object TimelineMomentViewModel : ViewModel() {
|
||||
private val momentService: MomentService = MomentServiceImpl()
|
||||
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
private val userService :UserService = UserServiceImpl()
|
||||
val momentsFlow = _momentsFlow.asStateFlow()
|
||||
var existsMoment = mutableStateOf(false)
|
||||
var refreshing by mutableStateOf(false)
|
||||
var isFirstLoad = true
|
||||
fun refreshPager(pullRefresh: Boolean = false) {
|
||||
if (!isFirstLoad && !pullRefresh) {
|
||||
return
|
||||
}
|
||||
isFirstLoad = false
|
||||
viewModelScope.launch {
|
||||
if (pullRefresh) {
|
||||
refreshing = true
|
||||
}
|
||||
// 检查是否有动态
|
||||
val existMoments =
|
||||
momentService.getMoments(timelineId = AppState.UserId, pageNumber = 1)
|
||||
if (existMoments.list.isEmpty()) {
|
||||
existsMoment.value = true
|
||||
}
|
||||
if (pullRefresh) {
|
||||
refreshing = false
|
||||
}
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
// 如果没有动态,则显示热门动态
|
||||
timelineId = if (existMoments.list.isEmpty()) null else AppState.UserId,
|
||||
trend = if (existMoments.list.isEmpty()) true else null
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_momentsFlow.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLikeCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(likeCount = momentItem.likeCount + 1, liked = true)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun likeMoment(id: Int) {
|
||||
momentService.likeMoment(id)
|
||||
updateLikeCount(id)
|
||||
}
|
||||
|
||||
fun updateCommentCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(commentCount = momentItem.commentCount + 1)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun onAddComment(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
updateCommentCount(id)
|
||||
}
|
||||
|
||||
|
||||
fun updateDislikeMomentById(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(likeCount = momentItem.likeCount - 1, liked = false)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun dislikeMoment(id: Int) {
|
||||
momentService.dislikeMoment(id)
|
||||
updateDislikeMomentById(id)
|
||||
}
|
||||
|
||||
fun updateFavoriteCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(favoriteCount = momentItem.favoriteCount + 1, isFavorite = true)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun favoriteMoment(id: Int) {
|
||||
momentService.favoriteMoment(id)
|
||||
updateFavoriteCount(id)
|
||||
}
|
||||
|
||||
fun updateUnfavoriteCount(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(favoriteCount = momentItem.favoriteCount - 1, isFavorite = false)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun unfavoriteMoment(id: Int) {
|
||||
momentService.unfavoriteMoment(id)
|
||||
updateUnfavoriteCount(id)
|
||||
}
|
||||
|
||||
fun deleteMoment(id: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.filter { momentItem ->
|
||||
momentItem.id != id
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动态评论数
|
||||
*/
|
||||
fun updateMomentCommentCount(id: Int, delta: Int) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
momentItem.copy(commentCount = momentItem.commentCount + delta)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
fun updateFollowStatus(authorId:Int,isFollow:Boolean) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.authorId == authorId) {
|
||||
momentItem.copy(followStatus = isFollow)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
fun followAction(moment: MomentEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
if (moment.followStatus) {
|
||||
userService.unFollowUser(moment.authorId.toString())
|
||||
} else {
|
||||
userService.followUser(moment.authorId.toString())
|
||||
}
|
||||
updateFollowStatus(moment.authorId, !moment.followStatus)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ResetModel() {
|
||||
_momentsFlow.value = PagingData.empty()
|
||||
isFirstLoad = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.filter
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.Messaging
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.data.UploadImage
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object MyProfileViewModel : ViewModel() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
val momentService: MomentService = MomentServiceImpl()
|
||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||
private var _sharedFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
var sharedFlow = _sharedFlow.asStateFlow()
|
||||
|
||||
var refreshing by mutableStateOf(false)
|
||||
var firstLoad = true
|
||||
suspend fun loadUserProfile() {
|
||||
val profile = accountService.getMyAccountProfile()
|
||||
MyProfileViewModel.profile = profile
|
||||
}
|
||||
fun loadProfile(pullRefresh: Boolean = false) {
|
||||
if (!firstLoad && !pullRefresh) return
|
||||
viewModelScope.launch {
|
||||
if (pullRefresh) {
|
||||
refreshing = true
|
||||
}
|
||||
firstLoad = false
|
||||
loadUserProfile()
|
||||
refreshing = false
|
||||
profile?.let {
|
||||
try {
|
||||
// Collect shared flow
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
author = AppState.UserId
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_sharedFlow.value = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(context: Context) {
|
||||
viewModelScope.launch {
|
||||
Messaging.unregisterDevice(context)
|
||||
AppStore.apply {
|
||||
token = null
|
||||
rememberMe = false
|
||||
saveData()
|
||||
}
|
||||
// 删除推送渠道
|
||||
AppState.ReloadAppState(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateUserProfileBanner(bannerImageUrl: Uri?,file:File, context: Context) {
|
||||
viewModelScope.launch {
|
||||
val newBanner = bannerImageUrl?.let {
|
||||
val cursor = context.contentResolver.query(it, null, null, null, null)
|
||||
var newBanner: UploadImage? = null
|
||||
cursor?.use { cur ->
|
||||
val columnIndex = cur.getColumnIndex("_display_name")
|
||||
if (cur.moveToFirst() && columnIndex != -1) {
|
||||
val displayName = cur.getString(columnIndex)
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("Change banner", "File name: $displayName, extension: $extension")
|
||||
// read as file
|
||||
Log.d("Change banner", "File size: ${file.length()}")
|
||||
newBanner = UploadImage(file, displayName, it.toString(), extension)
|
||||
}
|
||||
}
|
||||
newBanner
|
||||
}
|
||||
accountService.updateProfile(
|
||||
banner = newBanner,
|
||||
avatar = null,
|
||||
nickName = null,
|
||||
bio = null
|
||||
)
|
||||
profile = accountService.getMyAccountProfile()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMoment(id: Int) {
|
||||
val currentPagingData = _sharedFlow.value
|
||||
val updatedPagingData = currentPagingData.filter { momentItem ->
|
||||
momentItem.id != id
|
||||
}
|
||||
_sharedFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
val bio get() = profile?.bio ?: ""
|
||||
val nickName get() = profile?.nickName ?: ""
|
||||
val avatar get() = profile?.avatar
|
||||
|
||||
fun ResetModel() {
|
||||
profile = null
|
||||
_sharedFlow.value = PagingData.empty()
|
||||
firstLoad = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
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
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.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.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.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.ravenow.AppState
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.MainActivity
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.DropdownMenu
|
||||
import com.aiosman.ravenow.ui.composables.MenuItem
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||
import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffold
|
||||
import com.aiosman.ravenow.ui.composables.toolbar.ScrollStrategy
|
||||
import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.EmptyMomentPostUnit
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryItem
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.MomentPostUnit
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserItem
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.delay
|
||||
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, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ProfileV3(
|
||||
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
|
||||
) {
|
||||
val model = MyProfileViewModel
|
||||
val state = rememberCollapsingToolbarScaffoldState()
|
||||
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||
var enabled by remember { mutableStateOf(true) }
|
||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var minibarExpanded by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = LocalNavController.current
|
||||
var bannerHeight = 400
|
||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||
context,
|
||||
scope,
|
||||
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
|
||||
quality = 100
|
||||
) { uri, file ->
|
||||
onUpdateBanner?.invoke(uri, file, context)
|
||||
}
|
||||
val moments = sharedFlow.collectAsLazyPagingItems()
|
||||
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||
model.loadProfile(pullRefresh = true)
|
||||
})
|
||||
var miniToolbarHeight by remember { mutableStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
var appTheme = LocalAppTheme.current
|
||||
var AppColors = appTheme
|
||||
var systemUiController = rememberSystemUiController()
|
||||
fun switchTheme(){
|
||||
// delay
|
||||
scope.launch {
|
||||
delay(200)
|
||||
AppState.switchTheme()
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||
if (AppState.darkMode) {
|
||||
(context as MainActivity).window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
|
||||
}else{
|
||||
(context as MainActivity).window.decorView.setBackgroundColor(android.graphics.Color.WHITE)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(refreshState)
|
||||
) {
|
||||
CollapsingToolbarScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.decentBackground),
|
||||
state = state,
|
||||
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
|
||||
toolbarScrollable = true,
|
||||
enabled = enabled,
|
||||
toolbar = { toolbarScrollState ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(miniToolbarHeight.dp)
|
||||
// 保持在最低高度和当前高度之间
|
||||
.background(AppColors.decentBackground)
|
||||
) {
|
||||
}
|
||||
// header
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.parallax(0.5f)
|
||||
.fillMaxWidth()
|
||||
.height(600.dp)
|
||||
.verticalScroll(toolbarScrollState)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
alpha = state.toolbarState.progress
|
||||
}
|
||||
) {
|
||||
// banner
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(bannerHeight.dp)
|
||||
.background(AppColors.decentBackground)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(bannerHeight.dp - 24.dp)
|
||||
.let {
|
||||
if (isSelf) {
|
||||
it.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
pickBannerImageLauncher.launch(this)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.shadow(
|
||||
elevation = 6.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))
|
||||
.background(AppColors.background.copy(alpha = 0.7f))
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.noRippleClickable {
|
||||
expanded = true
|
||||
},
|
||||
tint = AppColors.text
|
||||
)
|
||||
}
|
||||
val themeModeString =
|
||||
if (AppState.darkMode) R.string.light_mode else R.string.dark_mode
|
||||
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)
|
||||
}
|
||||
},
|
||||
MenuItem(
|
||||
stringResource(themeModeString),
|
||||
R.drawable.rider_pro_theme_mode_light
|
||||
) {
|
||||
expanded = false
|
||||
switchTheme()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.decentBackground)
|
||||
) {
|
||||
// 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(20.dp))
|
||||
profile?.let {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (isSelf) {
|
||||
SelfProfileAction {
|
||||
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||
}
|
||||
} else {
|
||||
if (it.id != AppState.UserId) {
|
||||
OtherProfileAction(
|
||||
it,
|
||||
onFollow = {
|
||||
onFollowClick()
|
||||
},
|
||||
onChat = {
|
||||
onChatClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
alpha = 1 - state.toolbarState.progress
|
||||
}
|
||||
.background(AppColors.decentBackground)
|
||||
.onGloballyPositioned {
|
||||
miniToolbarHeight = with(density) {
|
||||
it.size.height.toDp().value.toInt()
|
||||
}
|
||||
}
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.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))
|
||||
androidx.compose.material3.Text(
|
||||
text = profile?.nickName ?: "",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (isSelf) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.noRippleClickable {
|
||||
minibarExpanded = true
|
||||
},
|
||||
tint = AppColors.text
|
||||
)
|
||||
}
|
||||
val themeModeString =
|
||||
if (AppState.darkMode) R.string.light_mode else R.string.dark_mode
|
||||
DropdownMenu(
|
||||
expanded = minibarExpanded,
|
||||
onDismissRequest = { minibarExpanded = false },
|
||||
width = 250,
|
||||
menuItems = listOf(
|
||||
MenuItem(
|
||||
stringResource(R.string.logout),
|
||||
R.mipmap.rider_pro_logout
|
||||
) {
|
||||
minibarExpanded = 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
|
||||
) {
|
||||
minibarExpanded = false
|
||||
scope.launch {
|
||||
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
||||
}
|
||||
},
|
||||
MenuItem(
|
||||
stringResource(R.string.favourites),
|
||||
R.drawable.rider_pro_favourite
|
||||
) {
|
||||
minibarExpanded = false
|
||||
scope.launch {
|
||||
navController.navigate(NavigationRoute.FavouriteList.route)
|
||||
}
|
||||
},
|
||||
MenuItem(
|
||||
stringResource(themeModeString),
|
||||
R.drawable.rider_pro_theme_mode_light
|
||||
) {
|
||||
minibarExpanded = false
|
||||
switchTheme()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.decentBackground)
|
||||
) {
|
||||
UserContentPageIndicator(
|
||||
pagerState = pagerState,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
) { idx ->
|
||||
when (idx) {
|
||||
0 ->
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
8.dp
|
||||
),
|
||||
verticalItemSpacing = 8.dp,
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
if (isSelf) {
|
||||
items(1) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.75f)
|
||||
.clip(
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(
|
||||
AppColors.background
|
||||
)
|
||||
.padding(8.dp)
|
||||
.noRippleClickable {
|
||||
NewPostViewModel.asNewPost()
|
||||
navController.navigate(NavigationRoute.NewPost.route)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
.background(
|
||||
AppColors.decentBackground
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.align(Alignment.Center),
|
||||
tint = AppColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items(moments.itemCount) { idx ->
|
||||
val moment = moments[idx] ?: return@items
|
||||
GalleryItem(moment, idx)
|
||||
}
|
||||
items(2) {
|
||||
Spacer(modifier = Modifier.height(120.dp))
|
||||
}
|
||||
}
|
||||
|
||||
1 ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (moments.itemCount == 0 && isSelf) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, refreshState, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun ProfileWrap(
|
||||
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
MyProfileViewModel.loadProfile()
|
||||
}
|
||||
ProfileV3(
|
||||
onUpdateBanner = { uri, file, context ->
|
||||
MyProfileViewModel.updateUserProfileBanner(uri, file, context)
|
||||
},
|
||||
onLogout = {
|
||||
MyProfileViewModel.logout(context)
|
||||
},
|
||||
profile = MyProfileViewModel.profile,
|
||||
sharedFlow = MyProfileViewModel.sharedFlow,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
|
||||
@Composable
|
||||
fun GalleryItem(
|
||||
moment: MomentEntity,
|
||||
idx: Int = 0
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let {
|
||||
val firstImage = moment.images.firstOrNull()
|
||||
if (firstImage?.width != null &&
|
||||
firstImage.height != null &&
|
||||
firstImage.width!! > 0 &&
|
||||
firstImage.height!! > 0
|
||||
) {
|
||||
val ratio = firstImage.width!!.toFloat() / firstImage.height!!.toFloat()
|
||||
return@let it.aspectRatio(ratio.coerceIn(0.7f, 1.5f))
|
||||
} else {
|
||||
return@let it.aspectRatio(if (idx % 3 == 0) 1.5f else 1f)
|
||||
}
|
||||
}
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.noRippleClickable {
|
||||
navController.navigateToPost(
|
||||
moment.id
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
LocalContext.current,
|
||||
moment.images[0].thumbnail,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.exp.formatPostTime2
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||
|
||||
@Composable
|
||||
fun EmptyMomentPostUnit() {
|
||||
TimeGroup(stringResource(R.string.empty_my_post_title))
|
||||
ProfileEmptyMomentCard()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileEmptyMomentCard(
|
||||
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
var columnHeight by remember { mutableStateOf(0) }
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
||||
.width(14.dp)
|
||||
) {
|
||||
drawLine(
|
||||
color = Color(0xff899DA9),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, size.height),
|
||||
strokeWidth = 4f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
columnHeight = coordinates.size.height
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.empty_my_post_content), fontSize = 16.sp)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(3f / 2f)
|
||||
.background(Color.White)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.noRippleClickable {
|
||||
NewPostViewModel.asNewPost()
|
||||
navController.navigate(NavigationRoute.NewPost.route)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
tint = Color(0xFFD8D8D8),
|
||||
contentDescription = "New post",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentPostUnit(momentEntity: MomentEntity) {
|
||||
TimeGroup(momentEntity.time.formatPostTime2())
|
||||
ProfileMomentCard(
|
||||
momentEntity.momentTextContent,
|
||||
momentEntity.images[0].thumbnail,
|
||||
momentEntity.likeCount.toString(),
|
||||
momentEntity.commentCount.toString(),
|
||||
momentEntity = momentEntity
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimeGroup(time: String = "2024.06.08 12:23") {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.height(16.dp)
|
||||
.width(14.dp),
|
||||
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
|
||||
contentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = time,
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text,
|
||||
style = TextStyle(fontWeight = FontWeight.W600)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileMomentCard(
|
||||
content: String,
|
||||
imageUrl: String,
|
||||
like: String,
|
||||
comment: String,
|
||||
momentEntity: MomentEntity
|
||||
) {
|
||||
var columnHeight by remember { mutableStateOf(0) }
|
||||
val AppColors = LocalAppTheme.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
||||
.width(14.dp)
|
||||
) {
|
||||
drawLine(
|
||||
color = AppColors.divider,
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, size.height),
|
||||
strokeWidth = 4f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
AppColors.background)
|
||||
.weight(1f)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
columnHeight = coordinates.size.height
|
||||
}
|
||||
) {
|
||||
if (content.isNotEmpty()) {
|
||||
MomentCardTopContent(content)
|
||||
}
|
||||
MomentCardPicture(imageUrl, momentEntity = momentEntity)
|
||||
MomentCardOperation(like, comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentCardTopContent(content: String) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
|
||||
text = content, fontSize = 16.sp, color = AppColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.aspectRatio(3f / 2f)
|
||||
.padding(top = 16.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigateToPost(
|
||||
id = momentEntity.id,
|
||||
highlightCommentId = 0,
|
||||
initImagePagerIndex = 0
|
||||
)
|
||||
},
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentCardOperation(like: String, comment: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MomentCardOperationItem(
|
||||
drawable = R.drawable.rider_pro_like,
|
||||
number = like,
|
||||
modifier = Modifier.padding(end = 32.dp)
|
||||
)
|
||||
MomentCardOperationItem(
|
||||
drawable = R.drawable.rider_pro_moment_comment,
|
||||
number = comment,
|
||||
modifier = Modifier.padding(end = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
|
||||
painter = painterResource(id = drawable), contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Text(text = number, color = AppColors.text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
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 com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
@Composable
|
||||
fun OtherProfileAction(
|
||||
profile: AccountProfileEntity,
|
||||
onFollow: (() -> Unit)? = null,
|
||||
onChat: (() -> Unit)? = null
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(if (profile.isFollowing) AppColors.main else AppColors.basicMain)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.noRippleClickable {
|
||||
onFollow?.invoke()
|
||||
}
|
||||
) {
|
||||
if (profile.isFollowing) {
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = AppColors.mainText
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = AppColors.mainText
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(if (profile.isFollowing) R.string.unfollow_upper else R.string.follow_upper),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = if (profile.isFollowing) AppColors.mainText else AppColors.text,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(AppColors.basicMain)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.noRippleClickable {
|
||||
onChat?.invoke()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_comment),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.chat_upper),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
// 按钮
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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 com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
@Composable
|
||||
fun SelfProfileAction(
|
||||
onEditProfile: () -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
// 按钮
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(
|
||||
AppColors.basicMain
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.noRippleClickable {
|
||||
onEditProfile()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.edit_profile),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun UserContentPageIndicator(
|
||||
pagerState: PagerState
|
||||
){
|
||||
val scope = rememberCoroutineScope()
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(if (pagerState.currentPage == 0) AppColors.background else Color.Transparent)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.noRippleClickable {
|
||||
// switch to gallery
|
||||
scope.launch {
|
||||
pagerState.scrollToPage(0)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.gallery),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(if (pagerState.currentPage == 1) AppColors.background else Color.Transparent)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.noRippleClickable {
|
||||
// switch to moments
|
||||
scope.launch {
|
||||
pagerState.scrollToPage(1)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.moment),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.profile.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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 com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
@Composable
|
||||
fun UserItem(accountProfileEntity: AccountProfileEntity) {
|
||||
val navController = LocalNavController.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 头像
|
||||
CustomAsyncImage(
|
||||
LocalContext.current,
|
||||
accountProfileEntity.avatar,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(48.dp),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
//个人统计
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.FollowerList.route.replace(
|
||||
"{id}",
|
||||
accountProfileEntity.id.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = accountProfileEntity.followerCount.toString(),
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.followers_upper),
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.FollowingList.route.replace(
|
||||
"{id}",
|
||||
accountProfileEntity.id.toString()
|
||||
)
|
||||
)
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = accountProfileEntity.followingCount.toString(),
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.following_upper),
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
// 昵称
|
||||
Text(
|
||||
text = accountProfileEntity.nickName,
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// 个人简介
|
||||
if (accountProfileEntity.bio.isNotEmpty()){
|
||||
Text(
|
||||
text = accountProfileEntity.bio,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}else{
|
||||
Text(
|
||||
text = "No bio here.",
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToPost
|
||||
|
||||
|
||||
@OptIn( ExperimentalMaterialApi::class)
|
||||
@Preview
|
||||
@Composable
|
||||
fun DiscoverScreen() {
|
||||
val model = DiscoverViewModel
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val navigationBarPaddings =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||
LaunchedEffect(Unit) {
|
||||
DiscoverViewModel.refreshPager()
|
||||
}
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val state = rememberPullRefreshState(refreshing, onRefresh = {
|
||||
model.refreshPager()
|
||||
})
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pullRefresh(state)
|
||||
.padding(bottom = navigationBarPaddings)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().background(
|
||||
AppColors.background).padding(bottom = 10.dp)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
SearchButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
|
||||
) {
|
||||
SearchViewModel.requestFocus = true
|
||||
navController.navigate(NavigationRoute.Search.route)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
DiscoverView()
|
||||
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchButton(
|
||||
modifier: Modifier = Modifier,
|
||||
clickAction: () -> Unit = {}
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
AppColors.inputBackground)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.noRippleClickable {
|
||||
clickAction()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
tint = AppColors.inputHint
|
||||
)
|
||||
Box {
|
||||
Text(
|
||||
text = context.getString(R.string.search),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
color = AppColors.inputHint,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DiscoverView() {
|
||||
val model = DiscoverViewModel
|
||||
var dataFlow = model.discoverMomentsFlow
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
||||
// contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
items(moments.itemCount) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.padding(2.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.noRippleClickable {
|
||||
navController.navigateToPost(
|
||||
id = momentItem.id,
|
||||
highlightCommentId = 0,
|
||||
initImagePagerIndex = 0
|
||||
)
|
||||
}
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = momentItem.images[0].thumbnail,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
context = context
|
||||
)
|
||||
if (momentItem.images.size > 1) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, end = 8.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.rider_pro_picture_more),
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object DiscoverViewModel:ViewModel() {
|
||||
private val momentService: MomentService = MomentServiceImpl()
|
||||
private val _discoverMomentsFlow =
|
||||
MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
val discoverMomentsFlow = _discoverMomentsFlow.asStateFlow()
|
||||
var firstLoad = true
|
||||
fun refreshPager() {
|
||||
if (!firstLoad) {
|
||||
return
|
||||
}
|
||||
firstLoad = false
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
trend = true
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_discoverMomentsFlow.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
fun ResetModel(){
|
||||
firstLoad = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.MomentCard
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Preview
|
||||
@Composable
|
||||
fun SearchScreen() {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val model = SearchViewModel
|
||||
val categories = listOf(context.getString(R.string.moment), context.getString(R.string.users))
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(pageCount = { categories.size })
|
||||
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
val navigationBarPaddings =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val navController = LocalNavController.current
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (model.requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
model.requestFocus = false
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
.padding(bottom = navigationBarPaddings)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
AppColors.background
|
||||
)
|
||||
.padding(bottom = 10.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(statusBarPaddingValues.calculateTopPadding()))
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SearchInput(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
text = model.searchText,
|
||||
onTextChange = {
|
||||
model.searchText = it
|
||||
},
|
||||
onSearch = {
|
||||
model.search()
|
||||
// hide ime
|
||||
keyboardController?.hide() // Hide the keyboard
|
||||
},
|
||||
focusRequester = focusRequester
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
stringResource(R.string.cancel),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
navController.navigateUp()
|
||||
},
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (model.showResult) {
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTabIndex.value,
|
||||
backgroundColor = AppColors.background,
|
||||
contentColor = AppColors.text,
|
||||
) {
|
||||
categories.forEachIndexed { index, category ->
|
||||
Tab(
|
||||
selected = selectedTabIndex.value == index,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = { Text(category, color = AppColors.text) }
|
||||
)
|
||||
}
|
||||
}
|
||||
SearchPager(
|
||||
pagerState = pagerState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchInput(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String = "",
|
||||
onTextChange: (String) -> Unit = {},
|
||||
onSearch: () -> Unit = {},
|
||||
focusRequester: FocusRequester = remember { FocusRequester() }
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape = RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
AppColors.inputBackground
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = null,
|
||||
tint = AppColors.inputHint
|
||||
)
|
||||
Box {
|
||||
if (text.isEmpty()) {
|
||||
Text(
|
||||
text = context.getString(R.string.search),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
color = AppColors.inputHint,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
BasicTextField(
|
||||
value = text,
|
||||
onValueChange = {
|
||||
onTextChange(it)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = AppColors.text
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
onSearch()
|
||||
}
|
||||
),
|
||||
cursorBrush = SolidColor(AppColors.text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SearchPager(
|
||||
pagerState: PagerState,
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize().background(AppColors.background),
|
||||
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> MomentResultTab()
|
||||
1 -> UserResultTab()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MomentResultTab() {
|
||||
val model = SearchViewModel
|
||||
var dataFlow = model.momentsFlow
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
val AppColors = LocalAppTheme.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(moments.itemCount) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White)
|
||||
) {
|
||||
MomentCard(
|
||||
momentEntity = momentItem,
|
||||
hideAction = true,
|
||||
onFollowClick = {
|
||||
model.momentFollowAction(momentItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
// Spacer(modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserResultTab() {
|
||||
val model = SearchViewModel
|
||||
val users = model.usersFlow.collectAsLazyPagingItems()
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(users.itemCount) { idx ->
|
||||
val userItem = users[idx] ?: return@items
|
||||
UserItem(userItem) {
|
||||
scope.launch {
|
||||
if (userItem.isFollowing) {
|
||||
model.unfollowUser(userItem.id)
|
||||
} else {
|
||||
model.followUser(userItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserItem(
|
||||
accountProfile: AccountProfileEntity,
|
||||
onFollow: (AccountProfileEntity) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val AppColors = LocalAppTheme.current
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.noRippleClickable {
|
||||
navController.navigate("AccountProfile/${accountProfile.id}")
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl = accountProfile.avatar,
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.padding(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = accountProfile.nickName,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.search_user_item_follower_count,
|
||||
accountProfile.followerCount
|
||||
), fontSize = 14.sp, color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
// Box {
|
||||
// if (accountProfile.id != AppState.UserId) {
|
||||
// if (accountProfile.isFollowing) {
|
||||
// ActionButton(
|
||||
// text = stringResource(R.string.following_upper),
|
||||
// backgroundColor = Color(0xFF9E9E9E),
|
||||
// contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
|
||||
// color = Color.White,
|
||||
// fullWidth = false
|
||||
// ) {
|
||||
// onFollow(accountProfile)
|
||||
// }
|
||||
// } else {
|
||||
// ActionButton(
|
||||
// text = stringResource(R.string.follow_upper),
|
||||
// backgroundColor = Color(0xffda3832),
|
||||
// contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
|
||||
// color = Color.White,
|
||||
// fullWidth = false
|
||||
// ) {
|
||||
// onFollow(accountProfile)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.search
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.map
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.entity.AccountPagingSource
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object SearchViewModel : ViewModel() {
|
||||
var searchText by mutableStateOf("")
|
||||
private val momentService: MomentService = MomentServiceImpl()
|
||||
private val _momentsFlow = MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
val momentsFlow = _momentsFlow.asStateFlow()
|
||||
|
||||
private val userService = UserServiceImpl()
|
||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||
val usersFlow = _usersFlow.asStateFlow()
|
||||
var showResult by mutableStateOf(false)
|
||||
var requestFocus by mutableStateOf(false)
|
||||
fun search() {
|
||||
if (searchText.isEmpty()) {
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
contentSearch = searchText
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_momentsFlow.value = it
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
AccountPagingSource(
|
||||
userService,
|
||||
nickname = searchText
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_usersFlow.value = it
|
||||
}
|
||||
}
|
||||
showResult = true
|
||||
}
|
||||
|
||||
suspend fun followUser(id:Int){
|
||||
userService.followUser(id.toString())
|
||||
val currentPagingData = _usersFlow.value
|
||||
val updatedPagingData = currentPagingData.map { userItem ->
|
||||
if (userItem.id == id) {
|
||||
userItem.copy(isFollowing = true, followerCount = userItem.followerCount + 1)
|
||||
} else {
|
||||
userItem
|
||||
}
|
||||
}
|
||||
_usersFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
suspend fun unfollowUser(id:Int){
|
||||
userService.unFollowUser(id.toString())
|
||||
val currentPagingData = _usersFlow.value
|
||||
val updatedPagingData = currentPagingData.map { userItem ->
|
||||
if (userItem.id == id) {
|
||||
userItem.copy(isFollowing = false, followerCount = userItem.followerCount - 1)
|
||||
} else {
|
||||
userItem
|
||||
}
|
||||
}
|
||||
_usersFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
fun ResetModel(){
|
||||
_momentsFlow.value = PagingData.empty()
|
||||
_usersFlow.value = PagingData.empty()
|
||||
showResult = false
|
||||
}
|
||||
|
||||
fun updateMomentFollowStatus(authorId:Int,isFollow:Boolean) {
|
||||
val currentPagingData = _momentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { momentItem ->
|
||||
if (momentItem.authorId == authorId) {
|
||||
momentItem.copy(followStatus = isFollow)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}
|
||||
_momentsFlow.value = updatedPagingData
|
||||
}
|
||||
fun momentFollowAction(moment: MomentEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
if (moment.followStatus) {
|
||||
userService.unFollowUser(moment.authorId.toString())
|
||||
} else {
|
||||
userService.followUser(moment.authorId.toString())
|
||||
}
|
||||
updateMomentFollowStatus(moment.authorId, !moment.followStatus)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.shorts
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.ParentDataModifier
|
||||
import androidx.compose.ui.unit.Density
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class PagerState(
|
||||
currentPage: Int = 0,
|
||||
minPage: Int = 0,
|
||||
maxPage: Int = 0
|
||||
) {
|
||||
private var _minPage by mutableStateOf(minPage)
|
||||
var minPage: Int
|
||||
get() = _minPage
|
||||
set(value) {
|
||||
_minPage = value.coerceAtMost(_maxPage)
|
||||
_currentPage = _currentPage.coerceIn(_minPage, _maxPage)
|
||||
}
|
||||
|
||||
private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
|
||||
var maxPage: Int
|
||||
get() = _maxPage
|
||||
set(value) {
|
||||
_maxPage = value.coerceAtLeast(_minPage)
|
||||
_currentPage = _currentPage.coerceIn(_minPage, maxPage)
|
||||
}
|
||||
|
||||
private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
|
||||
var currentPage: Int
|
||||
get() = _currentPage
|
||||
set(value) {
|
||||
_currentPage = value.coerceIn(minPage, maxPage)
|
||||
}
|
||||
|
||||
enum class SelectionState { Selected, Undecided }
|
||||
|
||||
var selectionState by mutableStateOf(SelectionState.Selected)
|
||||
|
||||
suspend inline fun <R> selectPage(block: PagerState.() -> R): R = try {
|
||||
selectionState = SelectionState.Undecided
|
||||
block()
|
||||
} finally {
|
||||
selectPage()
|
||||
}
|
||||
|
||||
suspend fun selectPage() {
|
||||
currentPage -= currentPageOffset.roundToInt()
|
||||
snapToOffset(0f)
|
||||
selectionState = SelectionState.Selected
|
||||
}
|
||||
|
||||
private var _currentPageOffset = Animatable(0f).apply {
|
||||
updateBounds(-1f, 1f)
|
||||
}
|
||||
val currentPageOffset: Float
|
||||
get() = _currentPageOffset.value
|
||||
|
||||
suspend fun snapToOffset(offset: Float) {
|
||||
val max = if (currentPage == minPage) 0f else 1f
|
||||
val min = if (currentPage == maxPage) 0f else -1f
|
||||
_currentPageOffset.snapTo(offset.coerceIn(min, max))
|
||||
}
|
||||
|
||||
suspend fun fling(velocity: Float) {
|
||||
if (velocity < 0 && currentPage == maxPage) return
|
||||
if (velocity > 0 && currentPage == minPage) return
|
||||
|
||||
// 根据 fling 的方向滑动到下一页或上一页
|
||||
_currentPageOffset.animateTo(velocity)
|
||||
selectPage()
|
||||
}
|
||||
|
||||
override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
|
||||
"currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class PageData(val page: Int) : ParentDataModifier {
|
||||
override fun Density.modifyParentData(parentData: Any?): Any? = this@PageData
|
||||
}
|
||||
|
||||
private val Measurable.page: Int
|
||||
get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
|
||||
|
||||
@Composable
|
||||
fun Pager(
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState,
|
||||
orientation: Orientation = Orientation.Horizontal,
|
||||
offscreenLimit: Int = 2,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, // 新增水平对齐参数
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, // 新增垂直对齐参数
|
||||
content: @Composable PagerScope.() -> Unit
|
||||
) {
|
||||
var pageSize by remember { mutableStateOf(0) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Layout(
|
||||
content = {
|
||||
// 根据 offscreenLimit 计算页面范围
|
||||
val minPage = maxOf(state.currentPage - offscreenLimit, state.minPage)
|
||||
val maxPage = minOf(state.currentPage + offscreenLimit, state.maxPage)
|
||||
|
||||
for (page in minPage..maxPage) {
|
||||
val pageData = PageData(page)
|
||||
val scope = PagerScope(state, page)
|
||||
key(pageData) {
|
||||
Column(modifier = pageData) {
|
||||
scope.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier.draggable(
|
||||
orientation = orientation,
|
||||
onDragStarted = {
|
||||
state.selectionState = PagerState.SelectionState.Undecided
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
coroutineScope.launch {
|
||||
// 根据速度判断是否滑动到下一页
|
||||
val threshold = 1000f // 速度阈值,可调整
|
||||
if (velocity > threshold) {
|
||||
state.fling(1f) // 向右滑动
|
||||
} else if (velocity < -threshold) {
|
||||
state.fling(-1f) // 向左滑动
|
||||
} else {
|
||||
state.fling(0f) // 保持当前页
|
||||
}
|
||||
}
|
||||
},
|
||||
state = rememberDraggableState { dy ->
|
||||
coroutineScope.launch {
|
||||
with(state) {
|
||||
val pos = pageSize * currentPageOffset
|
||||
val max = if (currentPage == minPage) 0 else pageSize
|
||||
val min = if (currentPage == maxPage) 0 else -pageSize
|
||||
|
||||
// 直接将手指的位移应用到 currentPageOffset
|
||||
val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
|
||||
snapToOffset(newPos / pageSize)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
) { measurables, constraints ->
|
||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||
val currentPage = state.currentPage
|
||||
val offset = state.currentPageOffset
|
||||
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||
|
||||
measurables.forEach { measurable ->
|
||||
val placeable = measurable.measure(childConstraints)
|
||||
val page = measurable.page
|
||||
|
||||
// 根据对齐参数计算 x 和 y 位置
|
||||
val xPosition = when (horizontalAlignment) {
|
||||
Alignment.Start -> 0
|
||||
Alignment.CenterHorizontally -> (constraints.maxWidth - placeable.width) / 2
|
||||
Alignment.End -> constraints.maxWidth - placeable.width
|
||||
else -> 0
|
||||
}
|
||||
|
||||
val yPosition = when (verticalAlignment) {
|
||||
Alignment.Top -> 0
|
||||
Alignment.CenterVertically -> (constraints.maxHeight - placeable.height) / 2
|
||||
Alignment.Bottom -> constraints.maxHeight - placeable.height
|
||||
else -> 0
|
||||
}
|
||||
|
||||
if (currentPage == page) { // 只在当前页面设置 pageSize,避免不必要的设置
|
||||
pageSize = if (orientation == Orientation.Horizontal) {
|
||||
placeable.width
|
||||
} else {
|
||||
placeable.height
|
||||
}
|
||||
}
|
||||
|
||||
val isVisible = abs(page - (currentPage - offset)) <= 1
|
||||
|
||||
if (isVisible) {
|
||||
// 修正 x 的计算
|
||||
val xOffset = if (orientation == Orientation.Horizontal) {
|
||||
((page - currentPage) * pageSize + offset * pageSize).roundToInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// 使用 placeRelative 进行放置
|
||||
placeable.placeRelative(
|
||||
x = xPosition + xOffset,
|
||||
y = yPosition + if (orientation == Orientation.Vertical) ((page - (currentPage - offset)) * placeable.height).roundToInt() else 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PagerScope(
|
||||
private val state: PagerState,
|
||||
val page: Int
|
||||
) {
|
||||
val currentPage: Int
|
||||
get() = state.currentPage
|
||||
|
||||
val currentPageOffset: Float
|
||||
get() = state.currentPageOffset
|
||||
|
||||
val selectionState: PagerState.SelectionState
|
||||
get() = state.selectionState
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.shorts
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.aiosman.ravenow.ui.theme.RiderProTheme
|
||||
|
||||
val videoUrls = listOf(
|
||||
"https://api.rider-pro.com/test/shorts/1.mp4",
|
||||
"https://api.rider-pro.com/test/shorts/2.mp4",
|
||||
"https://api.rider-pro.com/test/shorts/3.mp4",
|
||||
"https://api.rider-pro.com/test/shorts/4.mp4",
|
||||
"https://api.rider-pro.com/test/shorts/5.webm",
|
||||
"https://api.rider-pro.com/test/shorts/6.webm",
|
||||
"https://api.rider-pro.com/test/shorts/7.webm",
|
||||
"https://api.rider-pro.com/test/shorts/8.webm",
|
||||
"https://api.rider-pro.com/test/shorts/9.webm",
|
||||
"https://api.rider-pro.com/test/shorts/10.webm",
|
||||
"https://api.rider-pro.com/test/shorts/11.webm",
|
||||
"https://api.rider-pro.com/test/shorts/12.webm",
|
||||
"https://api.rider-pro.com/test/shorts/13.webm",
|
||||
"https://api.rider-pro.com/test/shorts/14.webm",
|
||||
"https://api.rider-pro.com/test/shorts/15.webm",
|
||||
"https://api.rider-pro.com/test/shorts/16.webm",
|
||||
"https://api.rider-pro.com/test/shorts/17.webm",
|
||||
"https://api.rider-pro.com/test/shorts/18.webm",
|
||||
"https://api.rider-pro.com/test/shorts/19.webm",
|
||||
"https://api.rider-pro.com/test/shorts/20.webm",
|
||||
"https://api.rider-pro.com/test/shorts/21.webm",
|
||||
"https://api.rider-pro.com/test/shorts/22.webm",
|
||||
"https://api.rider-pro.com/test/shorts/23.webm",
|
||||
"https://api.rider-pro.com/test/shorts/24.webm",
|
||||
"https://api.rider-pro.com/test/shorts/25.webm",
|
||||
"https://api.rider-pro.com/test/shorts/26.webm",
|
||||
"https://api.rider-pro.com/test/shorts/27.webm",
|
||||
"https://api.rider-pro.com/test/shorts/28.webm",
|
||||
"https://api.rider-pro.com/test/shorts/29.webm",
|
||||
"https://api.rider-pro.com/test/shorts/30.webm",
|
||||
"https://api.rider-pro.com/test/shorts/31.webm",
|
||||
"https://api.rider-pro.com/test/shorts/32.webm",
|
||||
"https://api.rider-pro.com/test/shorts/33.webm",
|
||||
"https://api.rider-pro.com/test/shorts/34.webm",
|
||||
"https://api.rider-pro.com/test/shorts/35.webm",
|
||||
"https://api.rider-pro.com/test/shorts/36.webm",
|
||||
"https://api.rider-pro.com/test/shorts/37.webm",
|
||||
"https://api.rider-pro.com/test/shorts/38.webm",
|
||||
"https://api.rider-pro.com/test/shorts/39.webm",
|
||||
"https://api.rider-pro.com/test/shorts/40.webm",
|
||||
"https://api.rider-pro.com/test/shorts/41.webm",
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ShortVideo() {
|
||||
RiderProTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
ShortViewCompose(
|
||||
videoItemsUrl = videoUrls,
|
||||
clickItemPosition = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.aiosman.ravenow.ui.index.tabs.shorts
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ShortViewCompose(
|
||||
videoItemsUrl: List<String>,
|
||||
clickItemPosition: Int = 0,
|
||||
videoHeader: @Composable () -> Unit = {},
|
||||
videoBottom: @Composable () -> Unit = {}
|
||||
) {
|
||||
val pagerState: PagerState = run {
|
||||
remember {
|
||||
PagerState(clickItemPosition, 0, videoItemsUrl.size - 1)
|
||||
}
|
||||
}
|
||||
val initialLayout = remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
val pauseIconVisibleState = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
Pager(
|
||||
state = pagerState,
|
||||
orientation = Orientation.Vertical,
|
||||
offscreenLimit = 1
|
||||
) {
|
||||
pauseIconVisibleState.value = false
|
||||
SingleVideoItemContent(
|
||||
videoItemsUrl[page],
|
||||
pagerState,
|
||||
page,
|
||||
initialLayout,
|
||||
pauseIconVisibleState,
|
||||
videoHeader,
|
||||
videoBottom
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(clickItemPosition) {
|
||||
delay(300)
|
||||
initialLayout.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleVideoItemContent(
|
||||
videoUrl: String,
|
||||
pagerState: PagerState,
|
||||
pager: Int,
|
||||
initialLayout: MutableState<Boolean>,
|
||||
pauseIconVisibleState: MutableState<Boolean>,
|
||||
VideoHeader: @Composable() () -> Unit,
|
||||
VideoBottom: @Composable() () -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
VideoPlayer(videoUrl, pagerState, pager, pauseIconVisibleState)
|
||||
VideoHeader.invoke()
|
||||
Box(modifier = Modifier.align(Alignment.BottomStart)) {
|
||||
VideoBottom.invoke()
|
||||
}
|
||||
if (initialLayout.value) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun VideoPlayer(
|
||||
videoUrl: String,
|
||||
pagerState: PagerState,
|
||||
pager: Int,
|
||||
pauseIconVisibleState: MutableState<Boolean>,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
var sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
val exoPlayer = remember {
|
||||
ExoPlayer.Builder(context)
|
||||
.build()
|
||||
.apply {
|
||||
val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
|
||||
context,
|
||||
Util.getUserAgent(context, context.packageName)
|
||||
)
|
||||
val source = ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)))
|
||||
|
||||
this.prepare(source)
|
||||
}
|
||||
}
|
||||
if (pager == pagerState.currentPage) {
|
||||
exoPlayer.playWhenReady = true
|
||||
exoPlayer.play()
|
||||
} else {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||
exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
|
||||
// player box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
var playerView by remember { mutableStateOf<PlayerView?>(null) } // Store reference to PlayerView
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
// 创建一个 FrameLayout 作为容器
|
||||
FrameLayout(context).apply {
|
||||
// 设置背景颜色为黑色,用于显示黑边
|
||||
setBackgroundColor(Color.Black.toArgb())
|
||||
|
||||
// 创建 PlayerView 并添加到 FrameLayout 中
|
||||
val view = PlayerView(context).apply {
|
||||
hideController()
|
||||
useController = false
|
||||
player = exoPlayer
|
||||
resizeMode =
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT // 或 RESIZE_MODE_ZOOM
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
Gravity.CENTER
|
||||
)
|
||||
}
|
||||
addView(view)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.noRippleClickable {
|
||||
pauseIconVisibleState.value = true
|
||||
exoPlayer.pause()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
if (exoPlayer.isPlaying) {
|
||||
exoPlayer.pause()
|
||||
} else {
|
||||
pauseIconVisibleState.value = false
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (pauseIconVisibleState.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(80.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Release ExoPlayer when the videoUrl changes or the composable leaves composition
|
||||
DisposableEffect(videoUrl) {
|
||||
onDispose {
|
||||
exoPlayer.release()
|
||||
playerView?.player = null
|
||||
playerView = null // Release the reference to the PlayerView
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> {
|
||||
exoPlayer.pause() // 应用进入后台时暂停
|
||||
}
|
||||
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if (pager == pagerState.currentPage) {
|
||||
exoPlayer.play() // 返回前台且为当前页面时恢复播放
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// action buttons
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
UserAvatar()
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k")
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") {
|
||||
showCommentModal = true
|
||||
}
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "234")
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "677k")
|
||||
}
|
||||
}
|
||||
// info
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 8.dp)
|
||||
.background(color = Color.Gray),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.padding(start = 4.dp, end = 6.dp),
|
||||
painter = painterResource(id = R.drawable.rider_pro_video_location),
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
text = "USA",
|
||||
fontSize = 12.sp,
|
||||
color = Color.White,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "@Kevinlinpr",
|
||||
fontSize = 16.sp,
|
||||
color = Color.White,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp), // 确保Text占用可用宽度
|
||||
text = "Pedro Acosta to join KTM in 2025 on a multi-year deal! \uD83D\uDFE0",
|
||||
fontSize = 16.sp,
|
||||
color = Color.White,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||
overflow = TextOverflow.Ellipsis, // 超出范围时显示省略号
|
||||
maxLines = 2 // 最多显示两行
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showCommentModal) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showCommentModal = false },
|
||||
containerColor = Color.White,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
CommentModalContent() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserAvatar() {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.size(40.dp)
|
||||
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
||||
.clip(
|
||||
RoundedCornerShape(40.dp)
|
||||
), painter = painterResource(id = R.drawable.default_avatar), contentDescription = ""
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoBtn(@DrawableRes icon: Int, text: String, onClick: (() -> Unit)? = null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.clickable {
|
||||
onClick?.invoke()
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(36.dp),
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 11.sp,
|
||||
color = Color.White,
|
||||
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.street
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavOptions
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.test.countries
|
||||
import com.google.android.gms.maps.model.CameraPosition
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.maps.android.compose.GoogleMap
|
||||
import com.google.maps.android.compose.MapProperties
|
||||
import com.google.maps.android.compose.MapUiSettings
|
||||
import com.google.maps.android.compose.MarkerComposable
|
||||
import com.google.maps.android.compose.MarkerState
|
||||
import com.google.maps.android.compose.rememberCameraPositionState
|
||||
|
||||
@Composable
|
||||
fun StreetPage() {
|
||||
val navController = LocalNavController.current
|
||||
var currentLocation by remember { mutableStateOf<LatLng?>(null) }
|
||||
val navigationBarHeight = with(LocalDensity.current) {
|
||||
WindowInsets.navigationBars.getBottom(this).toDp()
|
||||
}
|
||||
val cameraPositionState = rememberCameraPositionState {
|
||||
position = CameraPosition.fromLatLngZoom(currentLocation ?: LatLng(0.0, 0.0), 10f)
|
||||
}
|
||||
var hasLocationPermission by remember { mutableStateOf(false) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
}
|
||||
LaunchedEffect(currentLocation) {
|
||||
cameraPositionState.position =
|
||||
CameraPosition.fromLatLngZoom(
|
||||
currentLocation ?: LatLng(
|
||||
countries[0].lat,
|
||||
countries[0].lng
|
||||
), 5f
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 56.dp + navigationBarHeight)
|
||||
) {
|
||||
GoogleMap(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraPositionState = cameraPositionState,
|
||||
properties = MapProperties(
|
||||
isMyLocationEnabled = hasLocationPermission,
|
||||
),
|
||||
uiSettings = MapUiSettings(
|
||||
compassEnabled = true,
|
||||
myLocationButtonEnabled = false,
|
||||
zoomControlsEnabled = false
|
||||
)
|
||||
) {
|
||||
// pins
|
||||
countries.forEach { position ->
|
||||
MarkerComposable(
|
||||
state = MarkerState(position = LatLng(position.lat, position.lng)),
|
||||
onClick = {
|
||||
val screenLocation =
|
||||
cameraPositionState.projection?.toScreenLocation(it.position)
|
||||
val x = screenLocation?.x ?: 0
|
||||
val y = screenLocation?.y ?: 0
|
||||
|
||||
navController.navigate("LocationDetail/${x}/${y}",NavOptions.Builder()
|
||||
.setEnterAnim(0)
|
||||
.setExitAnim(0)
|
||||
.setPopEnterAnim(0)
|
||||
.setPopExitAnim(0)
|
||||
.build())
|
||||
true
|
||||
},
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_map_mark),
|
||||
contentDescription = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_my_location),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 16.dp, bottom = 16.dp + navigationBarHeight)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
currentLocation?.let {
|
||||
cameraPositionState.position =
|
||||
CameraPosition.fromLatLngZoom(it, cameraPositionState.position.zoom)
|
||||
}
|
||||
}
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp, bottom = 16.dp + navigationBarHeight)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = Color(0xffda3832))
|
||||
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(36.dp),
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 64.dp, start = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color.White)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color(0xfff7f7f7))
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_search_location),
|
||||
contentDescription = "",
|
||||
tint = Color(0xffc6c6c6),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
if (searchText.isEmpty()) {
|
||||
Text(
|
||||
text = "Please enter a search location",
|
||||
color = Color(0xffc6c6c6),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
textStyle = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user