调整我的派币界面ui,添加缺省图,修复bug,以及其他更改

移除其他用户界面右上角的积分显示

修复暂停视频后点赞/关注视频动态,暂停图标会消失以及关注/取消关注、收藏/取消收藏视频动态时用户头像会闪烁

修复发布动态/评论时' " & < >符号会变为&#39; &#34; &amp; &lt; &gt

修复短视频/新闻/推荐界面点击评论图标打开评论弹框显示异常

删除我的界面点壁纸可以更换壁纸的功能

根据设计图调整我的派币界面,增加返回按钮,添加缺省图,优化标签间的切换
This commit is contained in:
2025-11-13 17:02:32 +08:00
parent 83ef3e8dce
commit dbaa2f4c22
37 changed files with 1082 additions and 269 deletions

View File

@@ -447,7 +447,7 @@ fun MomentContentGroup(
}
if (!momentEntity.momentTextContent.isNullOrEmpty()) {
Text(
text = momentEntity.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(momentEntity.momentTextContent),
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 8.dp),

View File

@@ -216,7 +216,7 @@ fun DiscoverView() {
// 文本内容区域,限制最大高度
if (!momentItem.momentTextContent.isNullOrEmpty()) {
androidx.compose.material3.Text(
text = momentItem.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(momentItem.momentTextContent),
modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp,
color = AppColors.text,

View File

@@ -10,6 +10,7 @@ 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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -47,27 +48,38 @@ fun FullArticleModal(
val context = LocalContext.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.9f // 90% 高度
// 根据屏幕高度计算最大高度,用于限制弹窗最大高度
// 小屏幕(< 600dp75%中等屏幕600-800dp70%,大屏幕(> 800dp65%
val maxSheetHeight = when {
screenHeight.value < 600 -> screenHeight * 0.75f
screenHeight.value <= 800 -> screenHeight * 0.7f
else -> screenHeight * 0.65f
}
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
// 确保弹窗从下往上展开
androidx.compose.runtime.LaunchedEffect(Unit) {
sheetState.expand()
}
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
sheetState = sheetState,
containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.fillMaxWidth()
.heightIn(max = maxSheetHeight)
) {
// 滚动内容
Column(
modifier = Modifier
.weight(1f)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// 新闻图片区域 - 固定高度和宽度
@@ -75,7 +87,6 @@ fun FullArticleModal(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.background(color = appColors.secondaryBackground)
) {
if (moment.images.isNotEmpty()) {
@@ -149,7 +160,9 @@ fun FullArticleModal(
// 帖子内容
NewsContent(
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: ""),
content = com.aiosman.ravenow.utils.Utils.unescapeHtml(
if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: "")
),
images = moment.images,
context = context
)

View File

@@ -9,6 +9,7 @@ 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
@@ -200,7 +201,9 @@ fun NewsCommentModal(
}
Column(
modifier = Modifier.background(AppColors.background)
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
Row(
modifier = Modifier
@@ -229,13 +232,10 @@ fun NewsCommentModal(
}
// 评论列表
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
LazyColumn {
item {
@@ -258,7 +258,6 @@ fun NewsCommentModal(
}
}
}
}
// 底部输入栏
Column(

View File

@@ -11,7 +11,9 @@ 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.heightIn
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -180,20 +182,35 @@ fun NewsScreen() {
if (showCommentModal && selectedMoment != null) {
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.67f // 三分之二高度
// 根据屏幕高度计算最大高度,用于限制弹窗最大高度
// 小屏幕(< 600dp75%中等屏幕600-800dp70%,大屏幕(> 800dp65%
val maxSheetHeight = when {
screenHeight.value < 600 -> screenHeight * 0.75f
screenHeight.value <= 800 -> screenHeight * 0.7f
else -> screenHeight * 0.65f
}
val commentSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
// 确保弹窗从下往上展开
LaunchedEffect(Unit) {
commentSheetState.expand()
}
ModalBottomSheet(
onDismissRequest = {
showCommentModal = false
},
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
sheetState = commentSheetState,
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = maxSheetHeight)
) {
NewsCommentModal(
postId = selectedMoment?.id,
@@ -211,6 +228,7 @@ fun NewsScreen() {
}
}
}
}
}
//单个新闻项
@@ -287,7 +305,9 @@ fun NewsItem(
// 新闻内容(超出使用省略号)
Text(
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: ""),
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(
if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: "")
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),

View File

@@ -137,7 +137,7 @@ fun PostRecommendationItem(
modifier = Modifier
.fillMaxWidth(0.8f)
.padding(top = 4.dp),
text = moment.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(moment.momentTextContent),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),

View File

@@ -283,7 +283,7 @@ fun VideoRecommendationItem(
modifier = Modifier
.fillMaxWidth(0.8f)
.padding(top = 4.dp),
text = moment.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(moment.momentTextContent),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),

View File

@@ -409,18 +409,6 @@ fun ProfileV3(
bottomEnd = 32.dp
)
)
.let {
if (isSelf && isMain) {
it.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
} else {
it
}
}
) {
CustomAsyncImage(
LocalContext.current,
@@ -773,7 +761,8 @@ fun TopNavigationBar(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
// 左侧:互动数据卡片
// 左侧:互动数据卡片(仅本人主页显示)
if (isSelf) {
Row(
modifier = Modifier
.height(24.dp)
@@ -787,9 +776,7 @@ fun TopNavigationBar(
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 8.dp)
.let {
if (isSelf) it.noRippleClickable { onPointsClick?.invoke() } else it
},
.noRippleClickable { onPointsClick?.invoke() },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@@ -801,11 +788,7 @@ fun TopNavigationBar(
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (isSelf) {
pointsBalanceState?.value?.balance?.let { numberFormat.format(it) } ?: "--"
} else {
numberFormat.format(interactionCount)
},
text = pointsBalanceState?.value?.balance?.let { numberFormat.format(it) } ?: "--",
fontSize = 14.sp,
fontWeight = FontWeight.W500,
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
@@ -814,6 +797,7 @@ fun TopNavigationBar(
}
Spacer(modifier = Modifier.width(16.dp))
}
// 中间:分享图标
Image(

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -252,9 +253,6 @@ fun ShortViewCompose(
val initialLayout = remember {
mutableStateOf(true)
}
val pauseIconVisibleState = remember {
mutableStateOf(false)
}
Pager(
modifier = Modifier
@@ -266,7 +264,6 @@ fun ShortViewCompose(
) {
// 使用 key 确保每个页面独立,避免状态混乱
androidx.compose.runtime.key(page) {
pauseIconVisibleState.value = false
val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) {
videoMoments[page]
} else {
@@ -284,7 +281,6 @@ fun ShortViewCompose(
pagerState = pagerState,
pager = page,
initialLayout = initialLayout,
pauseIconVisibleState = pauseIconVisibleState,
VideoHeader = videoHeader,
VideoBottom = videoBottom,
onLikeClick = onLikeClick,
@@ -312,7 +308,6 @@ private fun SingleVideoItemContent(
pagerState: PagerState,
pager: Int,
initialLayout: MutableState<Boolean>,
pauseIconVisibleState: MutableState<Boolean>,
VideoHeader: @Composable() () -> Unit = {},
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null,
@@ -323,6 +318,18 @@ private fun SingleVideoItemContent(
onAvatarClick: ((MomentEntity) -> Unit)? = null,
isPageVisible: Boolean = true
) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) {
mutableStateOf(false)
}
// 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) {
if (pager != pagerState.currentPage) {
pauseIconVisibleState.value = false
}
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -379,7 +386,13 @@ fun VideoPlayer(
val lifecycleOwner = LocalLifecycleOwner.current
val configuration = androidx.compose.ui.platform.LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.7f // 屏幕的70%高度
// 根据屏幕高度计算最大高度,用于限制弹窗最大高度
// 小屏幕(< 600dp75%中等屏幕600-800dp70%,大屏幕(> 800dp65%
val maxSheetHeight = when {
screenHeight.value < 600 -> screenHeight * 0.75f
screenHeight.value <= 800 -> screenHeight * 0.7f
else -> screenHeight * 0.65f
}
var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
@@ -417,18 +430,34 @@ fun VideoPlayer(
}
// 根据页面状态控制播放
// 使用 remember 保存上一次的当前页面,只在页面真正切换时才重置暂停状态
val previousCurrentPage = remember(pager) { mutableStateOf(pagerState.currentPage) }
LaunchedEffect(pager, pagerState.currentPage, isPageVisible) {
val isCurrentPage = pager == pagerState.currentPage && isPageVisible
val pageChanged = previousCurrentPage.value != pagerState.currentPage
if (isCurrentPage) {
// 当前页面且页面可见:恢复播放
// 当前页面且页面可见
if (pageChanged) {
// 页面切换了(从其他页面切换到当前页面),恢复播放并重置暂停状态
exoPlayer.playWhenReady = true
exoPlayer.play()
pauseIconVisibleState.value = false
previousCurrentPage.value = pagerState.currentPage
} else {
// 页面未切换,保持当前的播放/暂停状态
// 如果用户手动暂停了,不要自动恢复播放;如果正在播放,保持播放
if (!pauseIconVisibleState.value && !exoPlayer.isPlaying) {
exoPlayer.playWhenReady = true
exoPlayer.play()
}
}
} else {
// 非当前页面或页面不可见:立即暂停并停止准备播放
exoPlayer.playWhenReady = false
exoPlayer.pause()
pauseIconVisibleState.value = false
previousCurrentPage.value = pagerState.currentPage
}
}
// 视频播放器容器 - 确保每个视频页面都被正确裁剪
@@ -519,8 +548,8 @@ fun VideoPlayer(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (moment != null) {
// 使用 key 确保状态变化时重新组合
androidx.compose.runtime.key(moment.id, moment.isFavorite) {
// 使用 key 确保状态变化时重新组合,包含所有可能变化的状态以避免不必要的重组
// 移除 key让 Compose 自动处理重组,避免头像闪烁
UserAvatar(
avatarUrl = moment.avatar,
onClick = { onAvatarClick?.invoke(moment) }
@@ -551,7 +580,6 @@ fun VideoPlayer(
text = formatCount(moment.shareCount),
onClick = { onShareClick?.invoke(moment) }
)
}
} else {
UserAvatar()
VideoBtn(icon = R.drawable.rider_pro_moment_like, text = "0")
@@ -609,7 +637,7 @@ fun VideoPlayer(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
text = moment.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(moment.momentTextContent),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),
@@ -637,7 +665,7 @@ fun VideoPlayer(
Box(
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
.heightIn(max = maxSheetHeight)
) {
CommentModalContent(
postId = moment.id,
@@ -656,6 +684,8 @@ fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
// 使用 key 确保当 avatarUrl 不变时不会重新创建组件,避免闪烁
androidx.compose.runtime.key(avatarUrl) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
@@ -675,7 +705,8 @@ fun UserAvatar(
imageUrl = avatarUrl,
contentDescription = "用户头像",
modifier = Modifier.fillMaxSize(),
defaultRes = R.drawable.default_avatar
defaultRes = R.drawable.default_avatar,
showShimmer = false // 禁用 shimmer 效果,避免闪烁
)
} else {
Image(
@@ -684,6 +715,7 @@ fun UserAvatar(
)
}
}
}
}
// 格式化数字显示

View File

@@ -1281,7 +1281,7 @@ fun PostDetails(
) {
if (!momentEntity?.momentTextContent.isNullOrEmpty()) {
Text(
text = momentEntity?.momentTextContent ?: "",
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(momentEntity?.momentTextContent),
fontSize = 14.sp,
color = AppColors.text,
)
@@ -1396,7 +1396,7 @@ fun CommentItem(
Text(
text = commentEntity.comment,
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(commentEntity.comment),
fontSize = 13.sp,
maxLines = Int.MAX_VALUE,
softWrap = true,

View File

@@ -116,4 +116,22 @@ object Utils {
return compressedFile
}
/**
* HTML 反转义函数
* 将 HTML 实体编码转换为对应的字符
* 例如:&#39; -> ', &#34; -> ", &amp; -> &, &lt; -> <, &gt; -> >
*/
fun unescapeHtml(html: String?): String {
if (html == null) return ""
return html
.replace("&#39;", "'")
.replace("&#34;", "\"")
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&apos;", "'")
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB