Merge pull request #79 from Kevinlinpr/nagisa

调整我的派币界面ui,添加缺省图,修复bug,以及其他更改
This commit is contained in:
2025-11-14 12:04:09 +08:00
committed by GitHub
40 changed files with 1033 additions and 254 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-05T12:24:27.034893100Z"> <DropdownSelection timestamp="2025-11-11T06:03:31.167121900Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=c328a150" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=f800b364" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

31
.idea/gradle.xml generated
View File

@@ -1,20 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option> </option>
</component> </GradleProjectSettings>
</option>
</component>
</project> </project>

View File

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

View File

@@ -216,7 +216,7 @@ fun DiscoverView() {
// 文本内容区域,限制最大高度 // 文本内容区域,限制最大高度
if (!momentItem.momentTextContent.isNullOrEmpty()) { if (!momentItem.momentTextContent.isNullOrEmpty()) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = momentItem.momentTextContent ?: "", text = com.aiosman.ravenow.utils.Utils.unescapeHtml(momentItem.momentTextContent),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp, fontSize = 12.sp,
color = AppColors.text, 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.fillMaxSize
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.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -47,27 +48,38 @@ fun FullArticleModal(
val context = LocalContext.current val context = LocalContext.current
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp 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( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState( sheetState = sheetState,
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = appColors.background, containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) { ) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.heightIn(max = maxSheetHeight)
) { ) {
// 滚动内容 // 滚动内容
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// 新闻图片区域 - 固定高度和宽度 // 新闻图片区域 - 固定高度和宽度
@@ -75,7 +87,6 @@ fun FullArticleModal(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(250.dp) .height(250.dp)
.background(color = appColors.secondaryBackground) .background(color = appColors.secondaryBackground)
) { ) {
if (moment.images.isNotEmpty()) { if (moment.images.isNotEmpty()) {
@@ -149,11 +160,13 @@ fun FullArticleModal(
// 帖子内容 // 帖子内容
NewsContent( 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, images = moment.images,
context = context context = context
) )
Spacer(modifier = Modifier.height(200.dp)) Spacer(modifier = Modifier.height(200.dp))
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
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.navigationBars import androidx.compose.foundation.layout.navigationBars
@@ -200,7 +201,9 @@ fun NewsCommentModal(
} }
Column( Column(
modifier = Modifier.background(AppColors.background) modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -229,33 +232,29 @@ fun NewsCommentModal(
} }
// 评论列表 // 评论列表
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
) { ) {
Box( LazyColumn {
modifier = Modifier.fillMaxWidth() item {
) { CommentContent(
LazyColumn { viewModel = commentViewModel,
item { onLongClick = { comment ->
CommentContent( showCommentMenu = true
viewModel = commentViewModel, contextComment = comment
onLongClick = { comment -> },
showCommentMenu = true onReply = { parentComment, _, _, _ ->
contextComment = comment if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
}, debouncedNavigation {
onReply = { parentComment, _, _, _ -> navController.navigate(NavigationRoute.Login.route)
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
replyComment = parentComment
} }
} else {
replyComment = parentComment
} }
) }
} )
} }
} }
} }

View File

@@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.heightIn
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.WindowInsets
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.width
@@ -180,23 +182,38 @@ fun NewsScreen() {
if (showCommentModal && selectedMoment != null) { if (showCommentModal && selectedMoment != null) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp 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( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showCommentModal = false showCommentModal = false
}, },
sheetState = rememberModalBottomSheetState( sheetState = commentSheetState,
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = AppColors.background, containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) { ) {
NewsCommentModal( Box(
postId = selectedMoment?.id, modifier = Modifier
.fillMaxWidth()
.heightIn(max = maxSheetHeight)
) {
NewsCommentModal(
postId = selectedMoment?.id,
commentCount = selectedMoment?.commentCount ?: 0, commentCount = selectedMoment?.commentCount ?: 0,
onDismiss = { onDismiss = {
showCommentModal = false showCommentModal = false
@@ -207,7 +224,8 @@ fun NewsScreen() {
onCommentDeleted = { onCommentDeleted = {
selectedMoment?.id?.let { model.onDeleteComment(it) } selectedMoment?.id?.let { model.onDeleteComment(it) }
} }
) )
}
} }
} }
} }
@@ -287,7 +305,9 @@ fun NewsItem(
// 新闻内容(超出使用省略号) // 新闻内容(超出使用省略号)
Text( 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 modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),

View File

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

View File

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

View File

@@ -389,18 +389,6 @@ fun ProfileV3(
bottomEnd = 32.dp bottomEnd = 32.dp
) )
) )
.let {
if (isSelf && isMain) {
it.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
} else {
it
}
}
) { ) {
CustomAsyncImage( CustomAsyncImage(
LocalContext.current, LocalContext.current,

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.heightIn
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.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -252,9 +253,6 @@ fun ShortViewCompose(
val initialLayout = remember { val initialLayout = remember {
mutableStateOf(true) mutableStateOf(true)
} }
val pauseIconVisibleState = remember {
mutableStateOf(false)
}
Pager( Pager(
modifier = Modifier modifier = Modifier
@@ -266,7 +264,6 @@ fun ShortViewCompose(
) { ) {
// 使用 key 确保每个页面独立,避免状态混乱 // 使用 key 确保每个页面独立,避免状态混乱
androidx.compose.runtime.key(page) { androidx.compose.runtime.key(page) {
pauseIconVisibleState.value = false
val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) { val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) {
videoMoments[page] videoMoments[page]
} else { } else {
@@ -284,7 +281,6 @@ fun ShortViewCompose(
pagerState = pagerState, pagerState = pagerState,
pager = page, pager = page,
initialLayout = initialLayout, initialLayout = initialLayout,
pauseIconVisibleState = pauseIconVisibleState,
VideoHeader = videoHeader, VideoHeader = videoHeader,
VideoBottom = videoBottom, VideoBottom = videoBottom,
onLikeClick = onLikeClick, onLikeClick = onLikeClick,
@@ -312,7 +308,6 @@ private fun SingleVideoItemContent(
pagerState: PagerState, pagerState: PagerState,
pager: Int, pager: Int,
initialLayout: MutableState<Boolean>, initialLayout: MutableState<Boolean>,
pauseIconVisibleState: MutableState<Boolean>,
VideoHeader: @Composable() () -> Unit = {}, VideoHeader: @Composable() () -> Unit = {},
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null, VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null, onLikeClick: ((MomentEntity) -> Unit)? = null,
@@ -323,6 +318,18 @@ private fun SingleVideoItemContent(
onAvatarClick: ((MomentEntity) -> Unit)? = null, onAvatarClick: ((MomentEntity) -> Unit)? = null,
isPageVisible: Boolean = true isPageVisible: Boolean = true
) { ) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) {
mutableStateOf(false)
}
// 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) {
if (pager != pagerState.currentPage) {
pauseIconVisibleState.value = false
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -379,7 +386,13 @@ fun VideoPlayer(
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val configuration = androidx.compose.ui.platform.LocalConfiguration.current val configuration = androidx.compose.ui.platform.LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp 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 showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState( var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
@@ -417,18 +430,34 @@ fun VideoPlayer(
} }
// 根据页面状态控制播放 // 根据页面状态控制播放
// 使用 remember 保存上一次的当前页面,只在页面真正切换时才重置暂停状态
val previousCurrentPage = remember(pager) { mutableStateOf(pagerState.currentPage) }
LaunchedEffect(pager, pagerState.currentPage, isPageVisible) { LaunchedEffect(pager, pagerState.currentPage, isPageVisible) {
val isCurrentPage = pager == pagerState.currentPage && isPageVisible val isCurrentPage = pager == pagerState.currentPage && isPageVisible
val pageChanged = previousCurrentPage.value != pagerState.currentPage
if (isCurrentPage) { if (isCurrentPage) {
// 当前页面且页面可见:恢复播放 // 当前页面且页面可见
exoPlayer.playWhenReady = true if (pageChanged) {
exoPlayer.play() // 页面切换了(从其他页面切换到当前页面),恢复播放并重置暂停状态
pauseIconVisibleState.value = false exoPlayer.playWhenReady = true
exoPlayer.play()
pauseIconVisibleState.value = false
previousCurrentPage.value = pagerState.currentPage
} else {
// 页面未切换,保持当前的播放/暂停状态
// 如果用户手动暂停了,不要自动恢复播放;如果正在播放,保持播放
if (!pauseIconVisibleState.value && !exoPlayer.isPlaying) {
exoPlayer.playWhenReady = true
exoPlayer.play()
}
}
} else { } else {
// 非当前页面或页面不可见:立即暂停并停止准备播放 // 非当前页面或页面不可见:立即暂停并停止准备播放
exoPlayer.playWhenReady = false exoPlayer.playWhenReady = false
exoPlayer.pause() exoPlayer.pause()
pauseIconVisibleState.value = false pauseIconVisibleState.value = false
previousCurrentPage.value = pagerState.currentPage
} }
} }
// 视频播放器容器 - 确保每个视频页面都被正确裁剪 // 视频播放器容器 - 确保每个视频页面都被正确裁剪
@@ -519,39 +548,38 @@ fun VideoPlayer(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (moment != null) { if (moment != null) {
// 使用 key 确保状态变化时重新组合 // 使用 key 确保状态变化时重新组合,包含所有可能变化的状态以避免不必要的重组
androidx.compose.runtime.key(moment.id, moment.isFavorite) { // 移除 key让 Compose 自动处理重组,避免头像闪烁
UserAvatar( UserAvatar(
avatarUrl = moment.avatar, avatarUrl = moment.avatar,
onClick = { onAvatarClick?.invoke(moment) } onClick = { onAvatarClick?.invoke(moment) }
) )
VideoBtn( VideoBtn(
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like, icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
text = formatCount(moment.likeCount), text = formatCount(moment.likeCount),
isActive = moment.liked, isActive = moment.liked,
onClick = { onLikeClick?.invoke(moment) } onClick = { onLikeClick?.invoke(moment) }
) )
VideoBtn( VideoBtn(
icon = R.mipmap.icon_comment, icon = R.mipmap.icon_comment,
text = formatCount(moment.commentCount), text = formatCount(moment.commentCount),
onClick = { onClick = {
showCommentModal = true showCommentModal = true
onCommentClick?.invoke(moment) onCommentClick?.invoke(moment)
} }
) )
VideoBtn( VideoBtn(
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect, icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
text = formatCount(moment.favoriteCount), text = formatCount(moment.favoriteCount),
isActive = false, // 收藏后不使用红色滤镜,保持图标原本颜色 isActive = false, // 收藏后不使用红色滤镜,保持图标原本颜色
keepOriginalColor = moment.isFavorite, // 收藏后保持原始颜色 keepOriginalColor = moment.isFavorite, // 收藏后保持原始颜色
onClick = { onFavoriteClick?.invoke(moment) } onClick = { onFavoriteClick?.invoke(moment) }
) )
VideoBtn( VideoBtn(
icon = R.mipmap.icon_share, icon = R.mipmap.icon_share,
text = formatCount(moment.shareCount), text = formatCount(moment.shareCount),
onClick = { onShareClick?.invoke(moment) } onClick = { onShareClick?.invoke(moment) }
) )
}
} else { } else {
UserAvatar() UserAvatar()
VideoBtn(icon = R.drawable.rider_pro_moment_like, text = "0") VideoBtn(icon = R.drawable.rider_pro_moment_like, text = "0")
@@ -609,7 +637,7 @@ fun VideoPlayer(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 4.dp), .padding(top = 4.dp),
text = moment.momentTextContent ?: "", text = com.aiosman.ravenow.utils.Utils.unescapeHtml(moment.momentTextContent),
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold), style = TextStyle(fontWeight = FontWeight.Bold),
@@ -637,7 +665,7 @@ fun VideoPlayer(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(sheetHeight) .heightIn(max = maxSheetHeight)
) { ) {
CommentModalContent( CommentModalContent(
postId = moment.id, postId = moment.id,
@@ -656,32 +684,36 @@ fun UserAvatar(
avatarUrl: String? = null, avatarUrl: String? = null,
onClick: (() -> Unit)? = null onClick: (() -> Unit)? = null
) { ) {
Box( // 使用 key 确保当 avatarUrl 不变时不会重新创建组件,避免闪烁
modifier = Modifier androidx.compose.runtime.key(avatarUrl) {
.padding(bottom = 16.dp) Box(
.size(40.dp) modifier = Modifier
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp)) .padding(bottom = 16.dp)
.clip(RoundedCornerShape(40.dp)) .size(40.dp)
.then( .border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
if (onClick != null) { .clip(RoundedCornerShape(40.dp))
Modifier.noRippleClickable { onClick() } .then(
} else { if (onClick != null) {
Modifier Modifier.noRippleClickable { onClick() }
} } else {
) Modifier
) { }
if (avatarUrl != null && avatarUrl.isNotEmpty()) { )
CustomAsyncImage( ) {
imageUrl = avatarUrl, if (avatarUrl != null && avatarUrl.isNotEmpty()) {
contentDescription = "用户头像", CustomAsyncImage(
modifier = Modifier.fillMaxSize(), imageUrl = avatarUrl,
defaultRes = R.drawable.default_avatar contentDescription = "用户头像",
) modifier = Modifier.fillMaxSize(),
} else { defaultRes = R.drawable.default_avatar,
Image( showShimmer = false // 禁用 shimmer 效果,避免闪烁
painter = painterResource(id = R.drawable.default_avatar), )
contentDescription = "用户头像" } else {
) Image(
painter = painterResource(id = R.drawable.default_avatar),
contentDescription = "用户头像"
)
}
} }
} }
} }

View File

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

View File

@@ -116,4 +116,22 @@ object Utils {
return compressedFile 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

View File

@@ -21,5 +21,4 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.daemon=true org.gradle.daemon=trueja
ja