@@ -0,0 +1,15 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MbtiBottomSheetHost() {
|
||||||
|
val show = MbtiSheetManager.visible.collectAsState(false).value
|
||||||
|
if (show) {
|
||||||
|
MbtiSelectBottomSheet(
|
||||||
|
onClose = { MbtiSheetManager.close() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,42 +1,60 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
// MBTI类型列表
|
// MBTI类型列表
|
||||||
val MBTI_TYPES = listOf(
|
val MBTI_TYPES = listOf(
|
||||||
@@ -46,96 +64,285 @@ val MBTI_TYPES = listOf(
|
|||||||
"ISTP", "ISFP", "ESTP", "ESFP"
|
"ISTP", "ISFP", "ESTP", "ESFP"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
|
||||||
|
return when {
|
||||||
|
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
|
||||||
|
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
|
||||||
|
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
|
||||||
|
else -> when (mbti) {
|
||||||
|
"INTJ" -> R.mipmap.mbti_intj
|
||||||
|
"INTP" -> R.mipmap.mbti_intp
|
||||||
|
"ENTJ" -> R.mipmap.mbti_entj
|
||||||
|
"ENTP" -> R.mipmap.mbti_entp
|
||||||
|
"INFJ" -> R.mipmap.mbti_infj
|
||||||
|
"INFP" -> R.mipmap.mbti_infp
|
||||||
|
"ENFJ" -> R.mipmap.mbti_enfj
|
||||||
|
"ENFP" -> R.mipmap.mbti_enfp
|
||||||
|
"ISTJ" -> R.mipmap.mbti_istj
|
||||||
|
"ISFJ" -> R.mipmap.mbti_isfj
|
||||||
|
"ESTJ" -> R.mipmap.mbti_estj
|
||||||
|
"ESFJ" -> R.mipmap.mbti_esfj
|
||||||
|
"ISTP" -> R.mipmap.mbti_istp
|
||||||
|
"ISFP" -> R.mipmap.mbti_isfp
|
||||||
|
"ESTP" -> R.mipmap.mbti_estp
|
||||||
|
"ESFP" -> R.mipmap.mbti_esfp
|
||||||
|
else -> R.mipmap.xingzuo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MbtiSelectScreen() {
|
fun MbtiSelectBottomSheet(
|
||||||
val navController = LocalNavController.current
|
onClose: () -> Unit
|
||||||
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
val isDarkMode = AppState.darkMode
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val currentMbti = model.mbti
|
val currentMbti = model.mbti
|
||||||
|
val sheetBackgroundColor = if (isDarkMode) {
|
||||||
|
appColors.secondaryBackground
|
||||||
|
} else {
|
||||||
|
Color(0xFFFFFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
// 确保弹窗展开
|
||||||
.background(appColors.profileBackground)
|
LaunchedEffect(Unit) {
|
||||||
|
sheetState.expand()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听状态变化,确保弹窗始终展开(防止拖拽关闭和滑动)
|
||||||
|
LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.isVisible) {
|
||||||
|
// 如果弹窗被拖拽关闭或位置发生变化,立即重新展开
|
||||||
|
if (!sheetState.isVisible || sheetState.targetValue != androidx.compose.material3.SheetValue.Expanded) {
|
||||||
|
kotlinx.coroutines.delay(10) // 短暂延迟确保状态更新
|
||||||
|
sheetState.expand()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onClose,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = sheetBackgroundColor,
|
||||||
|
dragHandle = null
|
||||||
) {
|
) {
|
||||||
// 头部
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 16.dp)
|
.fillMaxHeight(0.95f)
|
||||||
|
.offset(y = offsetY)
|
||||||
|
.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
Column(
|
||||||
title = stringResource(R.string.choose_mbti),
|
modifier = Modifier
|
||||||
moreIcon = false
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
// 头部 - 使用 Box 实现绝对居中布局
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val cancelButtonGradientColors = if (isDarkMode) {
|
||||||
|
listOf(
|
||||||
|
Color(0xFF3A3A3C),
|
||||||
|
Color(0xFF2C2C2E)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Color(0xFFFFFFFF),
|
||||||
|
Color(0xFFF8F8F8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
||||||
|
|
||||||
|
// 左上角返回按钮
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.height(36.dp)
|
||||||
|
.clip(RoundedCornerShape(18.dp))
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = cancelButtonGradientColors
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.noRippleClickable { onClose() }
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 左箭头图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(17.dp),
|
||||||
|
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
// "取消" 文字
|
||||||
|
Text(
|
||||||
|
text = "取消",
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = cancelButtonContentColor,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表
|
// 中间标题 - 绝对居中
|
||||||
LazyColumn(
|
Text(
|
||||||
modifier = Modifier.fillMaxSize(),
|
text = stringResource(R.string.choose_mbti),
|
||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
color = appColors.text,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 不消费任何事件,让 LazyVerticalGrid 先处理
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
// 不消费惯性滚动,让 LazyVerticalGrid 先处理
|
||||||
|
return Velocity.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
// 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网格列表 - 2列
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.nestedScroll(nestedScrollConnection),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
start = 8.dp,
|
||||||
|
top = 8.dp,
|
||||||
|
end = 8.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
items(MBTI_TYPES) { mbti ->
|
itemsIndexed(MBTI_TYPES) { index, mbti ->
|
||||||
MBTIItem(
|
MbtiItem(
|
||||||
mbti = mbti,
|
mbti = mbti,
|
||||||
isSelected = mbti == currentMbti,
|
isSelected = mbti == currentMbti,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// 保存MBTI类型
|
||||||
model.mbti = mbti
|
model.mbti = mbti
|
||||||
// 立即保存到本地存储,确保选择后立即生效
|
onClose()
|
||||||
AppState.UserId?.let { uid ->
|
|
||||||
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
|
|
||||||
}
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MBTIItem(
|
fun MbtiSelectScreen() {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
MbtiSelectBottomSheet(
|
||||||
|
onClose = {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MbtiItem(
|
||||||
mbti: String,
|
mbti: String,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
val isDarkMode = AppState.darkMode
|
||||||
|
|
||||||
Box(
|
// 卡片背景色
|
||||||
|
val cardBackgroundColor = if (isDarkMode) {
|
||||||
|
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
||||||
|
} else {
|
||||||
|
Color(0xFFFAF9FB)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.aspectRatio(1.1f)
|
||||||
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
|
.shadow(
|
||||||
|
elevation = if (isDarkMode) 8.dp else 2.dp,
|
||||||
|
shape = RoundedCornerShape(21.dp),
|
||||||
|
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(21.dp))
|
||||||
|
.background(cardBackgroundColor)
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(16.dp)
|
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Row(
|
// MBTI图标 - 使用占位图片
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Box(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
modifier = Modifier.size(100.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
|
||||||
|
contentDescription = mbti,
|
||||||
|
modifier = Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MBTI名称 - 使用负间距让文本向上移动,与图标更靠近
|
||||||
Text(
|
Text(
|
||||||
text = mbti,
|
text = mbti,
|
||||||
fontSize = 17.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Medium,
|
||||||
color = if (isSelected) appColors.main else appColors.text,
|
color = appColors.text,
|
||||||
modifier = Modifier.weight(1f)
|
textAlign = TextAlign.Center,
|
||||||
)
|
modifier = Modifier.offset(y = (-20).dp)
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = "Selected",
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = appColors.main
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
object MbtiSheetManager {
|
||||||
|
private val _visible = MutableStateFlow(false)
|
||||||
|
val visible: StateFlow<Boolean> = _visible.asStateFlow()
|
||||||
|
|
||||||
|
fun open() {
|
||||||
|
_visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
_visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -293,10 +293,6 @@ fun ZodiacSelectBottomSheet(
|
|||||||
onClick = {
|
onClick = {
|
||||||
// 保存当前语言的星座文本
|
// 保存当前语言的星座文本
|
||||||
model.zodiac = zodiacText
|
model.zodiac = zodiacText
|
||||||
// 立即保存到本地存储,确保选择后立即生效
|
|
||||||
AppState.UserId?.let { uid ->
|
|
||||||
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
|
|
||||||
}
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ import java.io.File
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
||||||
|
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
|
||||||
|
import com.aiosman.ravenow.ui.account.MbtiSheetManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑用户资料界面
|
* 编辑用户资料界面
|
||||||
@@ -194,6 +196,8 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
) {
|
) {
|
||||||
// 挂载星座选择弹窗
|
// 挂载星座选择弹窗
|
||||||
ZodiacBottomSheetHost()
|
ZodiacBottomSheetHost()
|
||||||
|
// 挂载MBTI选择弹窗
|
||||||
|
MbtiBottomSheetHost()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -425,9 +429,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
||||||
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
||||||
onClick = {
|
onClick = {
|
||||||
debouncedNavigation {
|
MbtiSheetManager.open()
|
||||||
navController.navigate(NavigationRoute.MbtiSelect.route)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -31,6 +32,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -79,8 +81,9 @@ class CommentModalViewModel(
|
|||||||
fun CommentModalContent(
|
fun CommentModalContent(
|
||||||
postId: Int? = null,
|
postId: Int? = null,
|
||||||
commentCount: Int = 0,
|
commentCount: Int = 0,
|
||||||
onCommentAdded: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
onDismiss: () -> Unit = {}
|
showTitle: Boolean = true,
|
||||||
|
onCommentAdded: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
@@ -161,6 +164,23 @@ fun CommentModalContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
// 拖动手柄
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(40.dp)
|
||||||
|
.height(4.dp)
|
||||||
|
.clip(RoundedCornerShape(50))
|
||||||
|
.background(AppColors.divider)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTitle) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -175,14 +195,11 @@ fun CommentModalContent(
|
|||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
HorizontalDivider(
|
|
||||||
color = AppColors.divider
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -17,15 +17,19 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -155,7 +159,10 @@ fun FavouriteListPage() {
|
|||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
if (momentItem.images.size > 1) {
|
|
||||||
|
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
||||||
|
|
||||||
|
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
@@ -168,6 +175,31 @@ fun FavouriteListPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isVideoMoment) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.4f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package com.aiosman.ravenow.ui.index
|
|||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.updateTransition
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -140,6 +136,7 @@ fun IndexScreen() {
|
|||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = drawerState.isOpen,
|
gesturesEnabled = drawerState.isOpen,
|
||||||
|
scrimColor = Color.Black.copy(alpha = 0.6f),
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
SideMenuContent(
|
SideMenuContent(
|
||||||
@@ -525,8 +522,6 @@ fun SideMenuContent(
|
|||||||
} else {
|
} else {
|
||||||
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
||||||
}
|
}
|
||||||
// 遮罩颜色 黑色透明度0.6
|
|
||||||
val overlayColor = Color.Black.copy(alpha = 0.6f)
|
|
||||||
// 卡片背景色 - 根据暗色模式适配
|
// 卡片背景色 - 根据暗色模式适配
|
||||||
val cardBackgroundColor = if (darkModeEnabled) {
|
val cardBackgroundColor = if (darkModeEnabled) {
|
||||||
appColors.background // 暗色模式:深色背景
|
appColors.background // 暗色模式:深色背景
|
||||||
@@ -546,24 +541,6 @@ fun SideMenuContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// 左侧半透明遮罩(平滑淡入淡出)
|
|
||||||
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
|
|
||||||
val overlayAlpha by overlayTransition.animateFloat(
|
|
||||||
transitionSpec = {
|
|
||||||
if (targetState) {
|
|
||||||
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
|
|
||||||
} else {
|
|
||||||
tween(durationMillis = 300, easing = FastOutLinearInEasing)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = "overlayAlpha"
|
|
||||||
) { open -> if (open) 0.6f else 0f }
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black.copy(alpha = overlayAlpha))
|
|
||||||
)
|
|
||||||
|
|
||||||
// 右侧菜单面板
|
// 右侧菜单面板
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
@@ -97,6 +99,9 @@ fun VideoRecommendationItem(
|
|||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
)
|
)
|
||||||
var pauseIconVisibleState by remember { mutableStateOf(false) }
|
var pauseIconVisibleState by remember { mutableStateOf(false) }
|
||||||
|
// 防抖:记录上次双击时间,防止快速重复双击
|
||||||
|
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
||||||
|
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
||||||
|
|
||||||
val exoPlayer = remember(videoUrl) {
|
val exoPlayer = remember(videoUrl) {
|
||||||
ExoPlayer.Builder(context)
|
ExoPlayer.Builder(context)
|
||||||
@@ -167,7 +172,19 @@ fun VideoRecommendationItem(
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.noRippleClickable {
|
.pointerInput(videoUrl, moment.liked) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = { offset ->
|
||||||
|
// 双击点赞/取消点赞
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
||||||
|
lastDoubleTapTime.value = currentTime
|
||||||
|
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
||||||
|
onLikeClick?.invoke(moment)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
// 单击播放/暂停
|
||||||
pauseIconVisibleState = true
|
pauseIconVisibleState = true
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -181,6 +198,8 @@ fun VideoRecommendationItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (pauseIconVisibleState) {
|
if (pauseIconVisibleState) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -300,7 +319,9 @@ fun VideoRecommendationItem(
|
|||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showCommentModal = false },
|
onDismissRequest = { showCommentModal = false },
|
||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
sheetState = sheetState
|
sheetState = sheetState,
|
||||||
|
dragHandle = {},
|
||||||
|
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
||||||
) {
|
) {
|
||||||
CommentModalContent(postId = moment.id) {
|
CommentModalContent(postId = moment.id) {
|
||||||
// 评论添加后的回调
|
// 评论添加后的回调
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -273,6 +276,45 @@ fun GalleryGrid(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 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 = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideoMoment) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.4f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = "",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -56,6 +57,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
@@ -467,6 +469,9 @@ fun VideoPlayer(
|
|||||||
.clip(RectangleShape)
|
.clip(RectangleShape)
|
||||||
) {
|
) {
|
||||||
var playerView by remember { mutableStateOf<PlayerView?>(null) }
|
var playerView by remember { mutableStateOf<PlayerView?>(null) }
|
||||||
|
// 防抖:记录上次双击时间,防止快速重复双击
|
||||||
|
val lastDoubleTapTime = remember { mutableStateOf(0L) }
|
||||||
|
val doubleTapDebounceTime = 500L // 500ms 防抖时间
|
||||||
|
|
||||||
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
|
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
|
||||||
androidx.compose.runtime.key(videoUrl) {
|
androidx.compose.runtime.key(videoUrl) {
|
||||||
@@ -479,11 +484,27 @@ fun VideoPlayer(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RectangleShape)
|
.clip(RectangleShape)
|
||||||
.noRippleClickable {
|
.pointerInput(videoUrl, moment?.liked) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = { offset ->
|
||||||
|
// 双击点赞/取消点赞
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
|
||||||
|
lastDoubleTapTime.value = currentTime
|
||||||
|
moment?.let {
|
||||||
|
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
|
||||||
|
onLikeClick?.invoke(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap = {
|
||||||
|
// 单击播放/暂停
|
||||||
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (pauseIconVisibleState.value) {
|
if (pauseIconVisibleState.value) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -660,7 +681,8 @@ fun VideoPlayer(
|
|||||||
},
|
},
|
||||||
containerColor = AppColors.background,
|
containerColor = AppColors.background,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
dragHandle = {},
|
||||||
|
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -670,6 +692,7 @@ fun VideoPlayer(
|
|||||||
CommentModalContent(
|
CommentModalContent(
|
||||||
postId = moment.id,
|
postId = moment.id,
|
||||||
commentCount = moment.commentCount,
|
commentCount = moment.commentCount,
|
||||||
|
showTitle = false,
|
||||||
onCommentAdded = {
|
onCommentAdded = {
|
||||||
onCommentAdded?.invoke(moment)
|
onCommentAdded?.invoke(moment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import androidx.compose.animation.slideInVertically
|
|||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
@@ -1159,14 +1161,26 @@ fun ImageViewerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PostImageView(
|
fun PostImageView(
|
||||||
images: List<MomentImageEntity>,
|
images: List<MomentImageEntity>,
|
||||||
initialPage: Int? = 0
|
initialPage: Int? = 0
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
var isImageViewerDialog by remember { mutableStateOf(false) }
|
var isImageViewerDialog by remember { mutableStateOf(false) }
|
||||||
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
|
val initialPageIndex = initialPage ?: 0
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
pageCount = { images.size },
|
||||||
|
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
|
||||||
|
)
|
||||||
|
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
|
||||||
|
|
||||||
|
// 同步 pagerState 的当前页面到 currentImageIndex
|
||||||
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
|
currentImageIndex = pagerState.currentPage
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -1187,14 +1201,21 @@ fun PostImageView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (images.isNotEmpty()) {
|
if (images.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
HorizontalPager(
|
||||||
context,
|
state = pagerState,
|
||||||
images[currentImageIndex].thumbnail,
|
|
||||||
contentDescription = "Image",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
) { page ->
|
||||||
|
val image = images[page]
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
image.thumbnail,
|
||||||
|
contentDescription = "Image",
|
||||||
|
blurHash = image.blurHash,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
@@ -1205,6 +1226,7 @@ fun PostImageView(
|
|||||||
.background(Color.Gray.copy(alpha = 0.1f))
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 图片导航控件
|
// 图片导航控件
|
||||||
if (images.size > 1) {
|
if (images.size > 1) {
|
||||||
@@ -1212,33 +1234,17 @@ fun PostImageView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Previous button
|
|
||||||
Text(
|
|
||||||
text = "Previous",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
if (currentImageIndex > 0) {
|
|
||||||
currentImageIndex--
|
|
||||||
}
|
|
||||||
},
|
|
||||||
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
|
|
||||||
)
|
|
||||||
|
|
||||||
// Indicators
|
// Indicators
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
images.forEachIndexed { index, _ ->
|
images.forEachIndexed { index, _ ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(4.dp)
|
.size(4.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
|
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
|
||||||
alpha = 0.5f
|
alpha = 0.5f
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1249,20 +1255,6 @@ fun PostImageView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next button
|
|
||||||
Text(
|
|
||||||
text = "Next",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
if (currentImageIndex < images.size - 1) {
|
|
||||||
currentImageIndex++
|
|
||||||
}
|
|
||||||
},
|
|
||||||
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/mipmap-hdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 1006 B |
BIN
app/src/main/res/mipmap-hdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 935 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 886 B |
BIN
app/src/main/res/mipmap-hdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 829 B |
BIN
app/src/main/res/mipmap-mdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
app/src/main/res/mipmap-mdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 916 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 882 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 800 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 919 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 852 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 793 B |
BIN
app/src/main/res/mipmap-mdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_infp.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_intj.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_intp.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_isfj.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_isfp.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_istj.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/mbti_istp.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/anmbti_entj.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/anmbti_entp.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/anmbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_enfj.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_enfp.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_entj.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_entp.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_esfj.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_esfp.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_estj.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_estp.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/mbti_infj.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |