feat: Implement image detail view and text/image mode switching

This commit introduces a dedicated image detail screen and integrates it with the post detail view, allowing users to switch between a text-focused layout and an immersive, image-centric one.

**Key Changes:**

*   **feat(ImageDetailScreen):**
    *   Added a new `ImageDetailScreen` for a full-screen, immersive image viewing experience.
    *   Implemented a `HorizontalPager` to allow swiping between multiple images.
    *   Included zoom (pinch-to-zoom) and double-tap-to-zoom functionality for images.
    *   Overlaid post information (author, content snippet) on a blurred background, which can be swiped up to switch back to the text detail view.
    *   The background is a blurred version of the current image, creating an ambient effect.

*   **feat(Navigation):**
    *   Created a `PostDetailContainer` to manage the animated transition between `PostDetailScreen` (text mode) and `ImageDetailScreen` (image mode).
    *   Updated the navigation route for `PostDetail` to accept a `mode` parameter (`text` or `image`) to handle deep linking directly into the image view.
    *   Clicking an image in the feed or post details now navigates to the image mode, preserving the shared element transition.

*   **refactor(PostDetail):**
    *   Modified `PostCardItem` and `PostDetailScreen` to pass both `postId` and the image `index` on image clicks.
    *   Refactored the image click handler to trigger the navigation to the new image detail mode.
This commit is contained in:
grtsinry43 2025-10-06 01:29:15 +08:00
parent 6a1bc7ad97
commit 93f95bf9c3
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
6 changed files with 694 additions and 25 deletions

View File

@ -6,8 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavType
@ -21,6 +20,7 @@ import com.qingshuige.tangyuan.ui.components.PageLevel
import com.qingshuige.tangyuan.ui.components.TangyuanBottomAppBar
import com.qingshuige.tangyuan.ui.components.TangyuanTopBar
import com.qingshuige.tangyuan.ui.screens.PostDetailScreen
import com.qingshuige.tangyuan.ui.screens.ImageDetailScreen
import com.qingshuige.tangyuan.ui.screens.TalkScreen
import com.qingshuige.tangyuan.ui.screens.LoginScreen
@ -40,6 +40,9 @@ fun App() {
onPostClick = { postId ->
navController.navigate(Screen.PostDetail.createRoute(postId))
},
onImageClick = { postId, imageIndex ->
navController.navigate(Screen.PostDetail.createRoute(postId, "image") + "?imageIndex=$imageIndex")
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@composable
)
@ -78,21 +81,24 @@ fun App() {
LoginScreen(navController = navController)
}
// 帖子详情页 - 只有共享元素动画,无页面切换动画
// 帖子详情页 - 统一容器管理两种模式
composable(
route = Screen.PostDetail.route,
arguments = listOf(navArgument("postId") { type = NavType.IntType })
arguments = listOf(
navArgument("postId") { type = NavType.IntType },
navArgument("mode") { type = NavType.StringType; defaultValue = "text" }
)
) { backStackEntry ->
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
PostDetailScreen(
val initialMode = backStackEntry.arguments?.getString("mode") ?: "text"
PostDetailContainer(
postId = postId,
initialMode = initialMode,
onBackClick = { navController.popBackStack() },
onAuthorClick = { authorId ->
// TODO: 导航到用户详情页
},
onImageClick = { imageUuid ->
// TODO: 导航到图片查看页
},
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@composable
)
@ -101,11 +107,99 @@ fun App() {
}
}
/**
* 帖子详情容器 - 统一管理文字和图片两种模式
* 保持与PostCard的共享元素动画同时支持内部模式切换动画
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun PostDetailContainer(
postId: Int,
initialMode: String,
onBackClick: () -> Unit,
onAuthorClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope
) {
// 本地状态管理模式切换
var currentMode by remember { mutableStateOf(initialMode) }
var imageIndex by remember { mutableIntStateOf(0) }
// 为模式切换创建内部AnimatedContent
AnimatedContent(
targetState = currentMode,
transitionSpec = {
// 使用滑动动画让切换更自然
when {
targetState == "image" && initialState == "text" -> {
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(400)
) togetherWith slideOutVertically(
targetOffsetY = { -it },
animationSpec = tween(400)
)
}
targetState == "text" && initialState == "image" -> {
slideInVertically(
initialOffsetY = { -it },
animationSpec = tween(400)
) togetherWith slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(400)
)
}
else -> {
fadeIn(animationSpec = tween(300)) togetherWith
fadeOut(animationSpec = tween(300))
}
}
},
label = "detail_mode_switch"
) { mode ->
when (mode) {
"image" -> {
ImageDetailScreen(
postId = postId,
initialImageIndex = imageIndex,
onBackClick = onBackClick,
onAuthorClick = onAuthorClick,
onSwitchToTextMode = {
currentMode = "text"
},
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = this@AnimatedContent
)
}
else -> {
PostDetailScreen(
postId = postId,
onBackClick = onBackClick,
onAuthorClick = onAuthorClick,
onImageClick = { _, selectedImageIndex ->
imageIndex = selectedImageIndex
currentMode = "image"
},
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = if (mode == initialMode) {
// 如果是初始模式使用外部的animatedContentScope来保持与PostCard的共享动画
animatedContentScope
} else {
// 如果是切换后的模式使用内部的AnimatedContent scope
this@AnimatedContent
}
)
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun MainFlow(
onLoginClick: () -> Unit,
onPostClick: (Int) -> Unit,
onImageClick: (Int, Int) -> Unit = { _, _ -> },
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
) {
@ -151,6 +245,7 @@ fun MainFlow(
onAuthorClick = { authorId ->
// TODO: 导航到用户详情页
},
onImageClick = onImageClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
)

View File

@ -6,7 +6,10 @@ sealed class Screen(val route: String, val title: String) {
object Topic : Screen("topic", "侃一侃")
object Message : Screen("message", "消息")
object User : Screen("settings", "我的")
object PostDetail : Screen("post_detail/{postId}", "帖子详情") {
fun createRoute(postId: Int) = "post_detail/$postId"
object PostDetail : Screen("post_detail/{postId}/{mode}", "帖子详情") {
fun createRoute(postId: Int, mode: String = "text") = "post_detail/$postId/$mode"
}
object ImageDetail : Screen("image_detail/{postId}/{imageIndex}", "图片详情") {
fun createRoute(postId: Int, imageIndex: Int) = "image_detail/$postId/$imageIndex"
}
}

View File

@ -132,6 +132,7 @@ fun PostCardItem(
onShareClick: (Int) -> Unit = {},
onBookmarkClick: (Int) -> Unit = {},
onMoreClick: (Int) -> Unit = {},
onImageClick: (Int, Int) -> Unit = { _, _ -> },
modifier: Modifier = Modifier,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
@ -185,7 +186,8 @@ fun PostCardItem(
Spacer(modifier = Modifier.height(12.dp))
PostCardImages(
imageUUIDs = postCard.imageUUIDs,
onImageClick = { /* TODO: 查看大图 */ }
postId = postCard.postId,
onImageClick = onImageClick
)
}
@ -299,7 +301,8 @@ private fun PostCardContent(postCard: PostCard) {
@Composable
private fun PostCardImages(
imageUUIDs: List<String>,
onImageClick: (String) -> Unit
postId: Int,
onImageClick: (Int, Int) -> Unit
) {
when (imageUUIDs.size) {
1 -> {
@ -312,7 +315,7 @@ private fun PostCardImages(
.height(200.dp)
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop,
onClick = { onImageClick(imageUUIDs[0]) }
onClick = { onImageClick(postId, 0) }
)
}
@ -321,7 +324,7 @@ private fun PostCardImages(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
imageUUIDs.forEach { uuid ->
imageUUIDs.forEachIndexed { index, uuid ->
ShimmerAsyncImage(
imageUrl = "${TangyuanApplication.instance.bizDomain}images/$uuid.jpg",
contentDescription = "文章图片",
@ -330,7 +333,7 @@ private fun PostCardImages(
.height(120.dp)
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop,
onClick = { onImageClick(uuid) }
onClick = { onImageClick(postId, index) }
)
}
}
@ -341,7 +344,7 @@ private fun PostCardImages(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
imageUUIDs.forEach { uuid ->
imageUUIDs.forEachIndexed { index, uuid ->
ShimmerAsyncImage(
imageUrl = "${TangyuanApplication.instance.bizDomain}images/$uuid.jpg",
contentDescription = "文章图片",
@ -350,7 +353,7 @@ private fun PostCardImages(
.height(100.dp)
.clip(MaterialTheme.shapes.medium),
contentScale = ContentScale.Crop,
onClick = { onImageClick(uuid) }
onClick = { onImageClick(postId, index) }
)
}
}

View File

@ -0,0 +1,562 @@
package com.qingshuige.tangyuan.ui.screens
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.qingshuige.tangyuan.TangyuanApplication
import com.qingshuige.tangyuan.model.PostCard
import com.qingshuige.tangyuan.ui.theme.LiteraryFontFamily
import com.qingshuige.tangyuan.ui.theme.TangyuanGeneralFontFamily
import com.qingshuige.tangyuan.ui.theme.TangyuanShapes
import com.qingshuige.tangyuan.utils.withPanguSpacing
import com.qingshuige.tangyuan.viewmodel.PostDetailViewModel
import kotlin.math.max
import kotlin.math.min
/**
* 图片详情页面 - 以图片为主的展示界面
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@Composable
fun ImageDetailScreen(
postId: Int,
initialImageIndex: Int = 0,
onBackClick: () -> Unit = {},
onAuthorClick: (Int) -> Unit = {},
onSwitchToTextMode: () -> Unit = {},
viewModel: PostDetailViewModel = hiltViewModel(),
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
) {
val state by viewModel.state.collectAsState()
// 加载帖子详情
LaunchedEffect(postId) {
viewModel.loadPostDetail(postId)
}
// 当有帖子数据时才显示内容
state.postCard?.let { postCard ->
val imageUUIDs = postCard.imageUUIDs
val validImageIndex = initialImageIndex.coerceIn(0, imageUUIDs.size - 1)
val pagerState = rememberPagerState(
initialPage = validImageIndex,
pageCount = { imageUUIDs.size }
)
Box(modifier = Modifier.fillMaxSize()) {
// 背景图片(模糊效果)
BackgroundBlurredImage(
imageUUIDs = imageUUIDs,
currentPage = pagerState.currentPage
)
// 主要内容
Column(modifier = Modifier.fillMaxSize()) {
// 顶部导航栏
ImageDetailTopBar(
onBackClick = onBackClick,
currentIndex = pagerState.currentPage + 1,
totalCount = imageUUIDs.size
)
// 图片轮播区域
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
if (imageUUIDs.isNotEmpty()) {
ImagePager(
imageUUIDs = imageUUIDs,
postId = postCard.postId,
pagerState = pagerState,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
)
}
}
// 底部内容区域(模糊遮罩)
BottomContentOverlay(
postCard = postCard,
onAuthorClick = onAuthorClick,
onSwitchToTextMode = onSwitchToTextMode
)
}
}
}
// 加载状态
if (state.isLoading && state.postCard == null) {
LoadingContent()
}
// 错误状态
state.error?.let { error ->
ErrorContent(
message = error,
onRetry = {
viewModel.clearError()
viewModel.loadPostDetail(postId)
}
)
}
}
/**
* 背景模糊图片
*/
@Composable
private fun BackgroundBlurredImage(
imageUUIDs: List<String>,
currentPage: Int
) {
if (imageUUIDs.isNotEmpty() && currentPage < imageUUIDs.size) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("${TangyuanApplication.instance.bizDomain}images/${imageUUIDs[currentPage]}.jpg")
.crossfade(true)
.build(),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.blur(radius = 20.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded),
contentScale = ContentScale.Crop,
alpha = 0.3f
)
}
// 渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Black.copy(alpha = 0.4f),
Color.Black.copy(alpha = 0.1f),
Color.Black.copy(alpha = 0.6f)
)
)
)
)
}
/**
* 顶部导航栏
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ImageDetailTopBar(
onBackClick: () -> Unit,
currentIndex: Int,
totalCount: Int
) {
TopAppBar(
title = {
Text(
text = "$currentIndex / $totalCount",
fontFamily = TangyuanGeneralFontFamily,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "返回",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
}
/**
* 图片轮播组件
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun ImagePager(
imageUUIDs: List<String>,
postId: Int,
pagerState: PagerState,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
ZoomableImage(
imageUrl = "${TangyuanApplication.instance.bizDomain}images/${imageUUIDs[page]}.jpg",
imageUuid = imageUUIDs[page],
postId = postId,
contentDescription = "图片 ${page + 1}",
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
)
}
}
/**
* 可缩放的图片组件
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun ZoomableImage(
imageUrl: String,
imageUuid: String,
postId: Int,
contentDescription: String,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
) {
var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val transformableState = rememberTransformableState { zoomChange, offsetChange, _ ->
scale = (scale * zoomChange).coerceIn(1f, 5f)
val maxX = (scale - 1) * 300
val maxY = (scale - 1) * 300
offset = Offset(
x = (offset.x + offsetChange.x).coerceIn(-maxX, maxX),
y = (offset.y + offsetChange.y).coerceIn(-maxY, maxY)
)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.build(),
contentDescription = contentDescription,
modifier = Modifier
.fillMaxSize()
.let { mod ->
if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
mod.sharedElement(
rememberSharedContentState(key = "post_card_$postId"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 500)
}
)
}
} else mod
}
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = offset.x,
translationY = offset.y
)
.transformable(state = transformableState)
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
scale = if (scale > 1f) 1f else 2f
offset = Offset.Zero
}
)
},
contentScale = ContentScale.Fit
)
}
}
/**
* 底部内容遮罩
*/
@Composable
private fun BottomContentOverlay(
postCard: PostCard,
onAuthorClick: (Int) -> Unit,
onSwitchToTextMode: () -> Unit
) {
var offsetY by remember { mutableFloatStateOf(0f) }
val swipeThreshold = -100f // 上滑超过100px触发切换
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.8f)
)
)
)
.offset(y = offsetY.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
if (offsetY < swipeThreshold) {
onSwitchToTextMode()
} else {
offsetY = 0f
}
}
) { _, dragAmount ->
offsetY = (offsetY + dragAmount.y).coerceAtMost(0f)
}
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
) {
// 作者信息
PostAuthorInfo(
postCard = postCard,
onAuthorClick = onAuthorClick
)
Spacer(modifier = Modifier.height(16.dp))
// 文章内容
Text(
text = postCard.textContent.withPanguSpacing(),
style = MaterialTheme.typography.bodyMedium.copy(
lineHeight = 22.sp
),
fontFamily = LiteraryFontFamily,
color = Color.White,
maxLines = 4,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 分类和时间
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = RoundedCornerShape(12.dp),
color = Color.White.copy(alpha = 0.2f)
) {
Text(
text = postCard.categoryName,
style = MaterialTheme.typography.labelMedium,
fontFamily = LiteraryFontFamily,
color = Color.White,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
fontWeight = FontWeight.Medium
)
}
Text(
text = postCard.getTimeDisplayText(),
style = MaterialTheme.typography.bodySmall,
fontFamily = TangyuanGeneralFontFamily,
color = Color.White.copy(alpha = 0.8f)
)
}
Spacer(modifier = Modifier.height(20.dp))
// 上滑提示 - 放在最下面居中
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
text = "上滑查看详情",
style = MaterialTheme.typography.labelSmall,
fontFamily = TangyuanGeneralFontFamily,
color = Color.White.copy(alpha = 0.4f)
)
}
}
}
}
/**
* 作者信息组件
*/
@Composable
private fun PostAuthorInfo(
postCard: PostCard,
onAuthorClick: (Int) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { onAuthorClick(postCard.authorId) }
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("${TangyuanApplication.instance.bizDomain}images/${postCard.authorAvatar}.jpg")
.crossfade(true)
.build(),
contentDescription = "${postCard.authorName}的头像",
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = postCard.authorName.withPanguSpacing(),
style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
if (postCard.authorBio.isNotBlank()) {
Text(
text = postCard.authorBio.withPanguSpacing(),
style = MaterialTheme.typography.bodySmall,
fontFamily = LiteraryFontFamily,
color = Color.White.copy(alpha = 0.8f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
/**
* 加载状态组件
*/
@Composable
private fun LoadingContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CircularProgressIndicator(
color = Color.White
)
Text(
text = "正在加载图片...",
style = MaterialTheme.typography.bodyMedium,
fontFamily = LiteraryFontFamily,
color = Color.White
)
}
}
}
/**
* 错误状态组件
*/
@Composable
private fun ErrorContent(
message: String,
onRetry: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(32.dp)
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(48.dp)
)
Text(
text = "加载失败",
style = MaterialTheme.typography.headlineSmall,
fontFamily = LiteraryFontFamily,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
fontFamily = LiteraryFontFamily,
color = Color.White.copy(alpha = 0.8f),
textAlign = TextAlign.Center
)
Button(
onClick = onRetry,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.2f)
)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "重试",
fontFamily = LiteraryFontFamily,
color = Color.White
)
}
}
}
}

View File

@ -51,7 +51,7 @@ fun PostDetailScreen(
postId: Int,
onBackClick: () -> Unit = {},
onAuthorClick: (Int) -> Unit = {},
onImageClick: (String) -> Unit = {},
onImageClick: (Int, Int) -> Unit = { _, _ -> },
viewModel: PostDetailViewModel = hiltViewModel(),
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
@ -181,7 +181,7 @@ private fun PostDetailContent(
isError: Boolean,
errorMessage: String?,
onAuthorClick: (Int) -> Unit,
onImageClick: (String) -> Unit,
onImageClick: (Int, Int) -> Unit,
onReplyToComment: (CommentCard) -> Unit,
onDeleteComment: (Int) -> Unit,
onRetry: () -> Unit,
@ -256,7 +256,7 @@ private fun PostDetailContent(
private fun PostDetailCard(
postCard: PostCard,
onAuthorClick: (Int) -> Unit,
onImageClick: (String) -> Unit,
onImageClick: (Int, Int) -> Unit,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
) {
@ -311,6 +311,7 @@ private fun PostDetailCard(
Spacer(modifier = Modifier.height(16.dp))
PostDetailImages(
imageUUIDs = postCard.imageUUIDs,
postId = postCard.postId,
onImageClick = onImageClick
)
}
@ -403,7 +404,8 @@ private fun PostDetailHeader(
@Composable
private fun PostDetailImages(
imageUUIDs: List<String>,
onImageClick: (String) -> Unit
postId: Int,
onImageClick: (Int, Int) -> Unit
) {
when (imageUUIDs.size) {
1 -> {
@ -416,7 +418,7 @@ private fun PostDetailImages(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable { onImageClick(imageUUIDs[0]) },
.clickable { onImageClick(postId, 0) },
contentScale = ContentScale.FillWidth
)
}
@ -425,10 +427,10 @@ private fun PostDetailImages(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 4.dp)
) {
items(imageUUIDs) { uuid ->
items(imageUUIDs.size) { index ->
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("${TangyuanApplication.instance.bizDomain}images/$uuid.jpg")
.data("${TangyuanApplication.instance.bizDomain}images/${imageUUIDs[index]}.jpg")
.crossfade(true)
.build(),
contentDescription = "文章图片",
@ -436,7 +438,7 @@ private fun PostDetailImages(
.width(200.dp)
.height(150.dp)
.clip(MaterialTheme.shapes.medium)
.clickable { onImageClick(uuid) },
.clickable { onImageClick(postId, index) },
contentScale = ContentScale.Crop
)
}

View File

@ -31,6 +31,7 @@ import kotlinx.coroutines.launch
fun TalkScreen(
onPostClick: (Int) -> Unit = {},
onAuthorClick: (Int) -> Unit = {},
onImageClick: (Int, Int) -> Unit = { _, _ -> },
viewModel: TalkViewModel = hiltViewModel(),
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
@ -101,6 +102,7 @@ fun TalkScreen(
onMoreClick = { postId ->
// TODO: 显示更多操作菜单
},
onImageClick = onImageClick,
onErrorDismiss = viewModel::clearError,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
@ -129,6 +131,7 @@ private fun PostList(
onShareClick: (Int) -> Unit,
onBookmarkClick: (Int) -> Unit,
onMoreClick: (Int) -> Unit,
onImageClick: (Int, Int) -> Unit,
onErrorDismiss: () -> Unit,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null
@ -151,6 +154,7 @@ private fun PostList(
onShareClick = onShareClick,
onBookmarkClick = onBookmarkClick,
onMoreClick = onMoreClick,
onImageClick = onImageClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
)