5 Commits

Author SHA1 Message Date
234587afc9 缁х画瀹屽杽鍔熻兘鏇存柊 2025-11-10 20:30:31 +08:00
b855acd8d3 Merge remote-tracking branch 'origin/main' into feat/pr-20251104-154907-clean 2025-11-10 19:48:35 +08:00
391f841f45 Merge pull request #59 from Kevinlinpr/atm2
feat: 新增短视频功能
2025-11-10 19:47:34 +08:00
2ed5639cbc feat: 新增短视频功能
- 新增短视频信息流页面,支持上下滑动切换视频。
- 实现视频播放、暂停、加载、空状态及错误处理等基础功能。
- 在视频页面中集成点赞、评论、收藏等互动操作。
- 后端接口新增 `videoFilter` 参数,用于仅获取包含视频的动态。
- 扩展了 `MomentEntity` 和相关数据模型,以支持视频数据。
- 将短视频页面集成到动态(Moment)Tab中。
2025-11-10 17:55:33 +08:00
2cbd2a975f Reapply "增加英文日语翻译 修改编辑资料界面无法更改星座mbit"
This reverts commit a057f7f7fd.
2025-11-10 15:26:31 +08:00
33 changed files with 1095 additions and 298 deletions

View File

@@ -4,6 +4,7 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentImageEntity
import com.aiosman.ravenow.entity.MomentVideoEntity
import com.google.gson.annotations.SerializedName
import java.io.File
@@ -12,8 +13,12 @@ data class Moment(
val id: Long,
@SerializedName("textContent")
val textContent: String,
@SerializedName("url")
val url: String? = null,
@SerializedName("images")
val images: List<Image>,
val images: List<Image>? = null,
@SerializedName("videos")
val videos: List<Video>? = null,
@SerializedName("user")
val user: User,
@SerializedName("likeCount")
@@ -24,7 +29,7 @@ data class Moment(
val favoriteCount: Long,
@SerializedName("isFavorite")
val isFavorite: Boolean,
@SerializedName("shareCount")
@SerializedName("isCommented")
val isCommented: Boolean,
@SerializedName("commentCount")
val commentCount: Long,
@@ -47,6 +52,14 @@ data class Moment(
val newsLanguage: String? = null,
@SerializedName("newsContent")
val newsContent: String? = null,
@SerializedName("hasFullText")
val hasFullText: Boolean = false,
@SerializedName("summary")
val summary: String? = null,
@SerializedName("publishedAt")
val publishedAt: String? = null,
@SerializedName("imageCached")
val imageCached: Boolean = false
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
@@ -62,7 +75,7 @@ data class Moment(
commentCount = commentCount.toInt(),
shareCount = 0,
favoriteCount = favoriteCount.toInt(),
images = images.map {
images = images?.map {
MomentImageEntity(
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
@@ -71,10 +84,28 @@ data class Moment(
width = it.width,
height = it.height
)
},
} ?: emptyList(),
authorId = user.id.toInt(),
liked = isLiked,
isFavorite = isFavorite,
url = url,
videos = videos?.map {
MomentVideoEntity(
id = it.id,
url = "${ApiClient.BASE_SERVER}${it.url}",
originalUrl = it.originalUrl,
directUrl = it.directUrl,
thumbnailUrl = it.thumbnailUrl?.let { thumb -> "${ApiClient.BASE_SERVER}$thumb" },
thumbnailDirectUrl = it.thumbnailDirectUrl,
duration = it.duration,
width = it.width,
height = it.height,
size = it.size,
format = it.format,
bitrate = it.bitrate,
frameRate = it.frameRate
)
},
// 新闻相关字段
isNews = isNews,
newsTitle = newsTitle ?: "",
@@ -82,7 +113,11 @@ data class Moment(
newsSource = newsSource ?: "",
newsCategory = newsCategory ?: "",
newsLanguage = newsLanguage ?: "",
newsContent = newsContent ?: ""
newsContent = newsContent ?: "",
hasFullText = hasFullText,
summary = summary,
publishedAt = publishedAt,
imageCached = imageCached
)
}
}
@@ -92,8 +127,26 @@ data class Image(
val id: Long,
@SerializedName("url")
val url: String,
@SerializedName("original_url")
val originalUrl: String? = null,
@SerializedName("directUrl")
val directUrl: String? = null,
@SerializedName("thumbnail")
val thumbnail: String,
@SerializedName("thumbnailDirectUrl")
val thumbnailDirectUrl: String? = null,
@SerializedName("small")
val small: String? = null,
@SerializedName("smallDirectUrl")
val smallDirectUrl: String? = null,
@SerializedName("medium")
val medium: String? = null,
@SerializedName("mediumDirectUrl")
val mediumDirectUrl: String? = null,
@SerializedName("large")
val large: String? = null,
@SerializedName("largeDirectUrl")
val largeDirectUrl: String? = null,
@SerializedName("blurHash")
val blurHash: String?,
@SerializedName("width")
@@ -102,13 +155,68 @@ data class Image(
val height: Int?
)
data class Video(
@SerializedName("id")
val id: Long,
@SerializedName("url")
val url: String,
@SerializedName("original_url")
val originalUrl: String? = null,
@SerializedName("directUrl")
val directUrl: String? = null,
@SerializedName("thumbnailUrl")
val thumbnailUrl: String? = null,
@SerializedName("thumbnailDirectUrl")
val thumbnailDirectUrl: String? = null,
@SerializedName("duration")
val duration: Int? = null,
@SerializedName("width")
val width: Int? = null,
@SerializedName("height")
val height: Int? = null,
@SerializedName("size")
val size: Long? = null,
@SerializedName("format")
val format: String? = null,
@SerializedName("bitrate")
val bitrate: Int? = null,
@SerializedName("frameRate")
val frameRate: String? = null
)
data class User(
@SerializedName("id")
val id: Long,
@SerializedName("nickName")
val nickName: String,
@SerializedName("avatar")
val avatar: String
val avatar: String,
@SerializedName("avatarMedium")
val avatarMedium: String? = null,
@SerializedName("avatarLarge")
val avatarLarge: String? = null,
@SerializedName("originAvatar")
val originAvatar: String? = null,
@SerializedName("avatarDirectUrl")
val avatarDirectUrl: String? = null,
@SerializedName("avatarMediumDirectUrl")
val avatarMediumDirectUrl: String? = null,
@SerializedName("avatarLargeDirectUrl")
val avatarLargeDirectUrl: String? = null,
@SerializedName("aiAccount")
val aiAccount: Boolean = false,
@SerializedName("aiRoleAvatar")
val aiRoleAvatar: String? = null,
@SerializedName("aiRoleAvatarMedium")
val aiRoleAvatarMedium: String? = null,
@SerializedName("aiRoleAvatarLarge")
val aiRoleAvatarLarge: String? = null,
@SerializedName("aiRoleAvatarDirectUrl")
val aiRoleAvatarDirectUrl: String? = null,
@SerializedName("aiRoleAvatarMediumDirectUrl")
val aiRoleAvatarMediumDirectUrl: String? = null,
@SerializedName("aiRoleAvatarLargeDirectUrl")
val aiRoleAvatarLargeDirectUrl: String? = null
)
data class UploadImage(

View File

@@ -800,6 +800,7 @@ interface RaveNowAPI {
@Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null,
@Query("newsFilter") newsFilter: String? = null,
@Query("videoFilter") videoFilter: String? = null,
): Response<ListContainer<Moment>>
@Multipart

View File

@@ -260,6 +260,38 @@ data class MomentImageEntity(
var height: Int? = null
)
/**
* 动态视频
*/
data class MomentVideoEntity(
// 视频ID
val id: Long,
// 视频URL
val url: String,
// 原始文件名
val originalUrl: String? = null,
// 直接访问URL
val directUrl: String? = null,
// 视频缩略图URL
val thumbnailUrl: String? = null,
// 视频缩略图直接访问URL
val thumbnailDirectUrl: String? = null,
// 视频时长(秒)
val duration: Int? = null,
// 宽度
val width: Int? = null,
// 高度
val height: Int? = null,
// 文件大小(字节)
val size: Long? = null,
// 视频格式
val format: String? = null,
// 视频比特率kbps
val bitrate: Int? = null,
// 帧率
val frameRate: String? = null
)
/**
* 动态
*/
@@ -300,6 +332,10 @@ data class MomentEntity(
var relMoment: MomentEntity? = null,
// 是否收藏
var isFavorite: Boolean = false,
// 外部链接
val url: String? = null,
// 动态视频列表
val videos: List<MomentVideoEntity>? = null,
// 新闻相关字段
val isNews: Boolean = false,
val newsTitle: String = "",
@@ -307,13 +343,22 @@ data class MomentEntity(
val newsSource: String = "",
val newsCategory: String = "",
val newsLanguage: String = "",
val newsContent: String = ""
val newsContent: String = "",
// 是否已获取完整正文
val hasFullText: Boolean = false,
// 新闻摘要
val summary: String? = null,
// 新闻发布时间
val publishedAt: String? = null,
// 是否已缓存图片
val imageCached: Boolean = false
)
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null,
val newsOnly: Boolean? = null
val newsOnly: Boolean? = null,
val videoOnly: Boolean? = null
)
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData(
@@ -327,7 +372,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId,
authorId = extra.authorId,
newsFilter = if (extra.newsOnly == true) "news_only" else ""
newsFilter = if (extra.newsOnly == true) "news_only" else "",
videoFilter = if (extra.videoOnly == true) "video_only" else ""
)
val data = result.body()?.let {
ListContainer(

View File

@@ -21,6 +21,8 @@ object AccountEditViewModel : ViewModel() {
var name by mutableStateOf("")
var bio by mutableStateOf("")
var imageUrl by mutableStateOf<Uri?>(null)
var bannerImageUrl by mutableStateOf<Uri?>(null)
var bannerFile by mutableStateOf<File?>(null)
val accountService: AccountService = AccountServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null)
var croppedBitmap by mutableStateOf<Bitmap?>(null)
@@ -82,6 +84,30 @@ object AccountEditViewModel : ViewModel() {
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
UploadImage(file, "avatar.jpg", "", "jpg")
}
// 处理背景图更新
val newBanner = bannerImageUrl?.let { uri ->
bannerFile?.let { file ->
val cursor = context.contentResolver.query(uri, null, null, null, null)
var uploadBanner: UploadImage? = null
cursor?.use { cur ->
val columnIndex = cur.getColumnIndex("_display_name")
if (cur.moveToFirst() && columnIndex != -1) {
val displayName = cur.getString(columnIndex)
val extension = displayName.substringAfterLast(".")
Log.d("AccountEditViewModel", "Banner file name: $displayName, extension: $extension")
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
} else {
// 如果无法获取文件名,使用默认值
val displayName = "banner.jpg"
val extension = "jpg"
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
}
}
uploadBanner
}
}
// 去除换行符,确保昵称和个人简介不包含换行
val cleanName = name.trim().replace("\n", "").replace("\r", "")
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
@@ -89,7 +115,7 @@ object AccountEditViewModel : ViewModel() {
val newName = if (cleanName == profile?.nickName) null else cleanName
accountService.updateProfile(
avatar = newAvatar,
banner = null,
banner = newBanner,
nickName = newName,
bio = cleanBio
)
@@ -100,6 +126,9 @@ object AccountEditViewModel : ViewModel() {
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
}
} catch (_: Exception) { }
// 清除背景图状态
bannerImageUrl = null
bannerFile = null
// 刷新用户资料
reloadProfile()
// 刷新个人资料页面的用户资料
@@ -116,6 +145,8 @@ object AccountEditViewModel : ViewModel() {
name = ""
bio = ""
imageUrl = null
bannerImageUrl = null
bannerFile = null
croppedBitmap = null
isUpdating = false
isLoading = false

View File

@@ -72,6 +72,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import android.widget.Toast
import java.io.File
/**
@@ -97,6 +98,10 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
quality = 100
) { uri, file ->
// 处理选中的图片
// 保存到 ViewModel 中,等待保存时一起上传
model.bannerImageUrl = uri
model.bannerFile = file
// 如果提供了回调,也调用它(用于个人主页直接更新)
onUpdateBanner?.invoke(uri, file, context)
}
@@ -104,6 +109,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.name = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
usernameError = when {
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
@@ -112,18 +118,37 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
}
}
fun validateNickname(): String? {
val cleanValue = model.name.replace("\n", "").replace("\r", "")
return when {
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
cleanValue.length > 20 -> context.getString(R.string.error_nickname_too_long)
else -> null
}
}
val appColors = LocalAppTheme.current
fun onBioChange(value: String) {
// 去除换行符,确保个人简介不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.bio = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
bioError = when {
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
else -> null
}
}
fun validateBio(): String? {
val cleanValue = model.bio.replace("\n", "").replace("\r", "")
return when {
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
else -> null
}
}
fun validate(): Boolean {
return usernameError == null && bioError == null
}
@@ -146,21 +171,21 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
model.reloadProfile()
}
// 设置状态栏为透明,使用浅色图标(因为顶部背景是深色图片)
// 设置状态栏为透明,根据暗色模式决定图标颜色
val systemUiController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
}
StatusBarMaskLayout(
modifier = Modifier.background(Color(0xFFFAF9FB)),
darkIcons = false, // 浅色图标(白色),因为顶部背景是深
modifier = Modifier.background(appColors.background),
darkIcons = !AppState.darkMode, // 根据暗色模式决定图标颜
maskBoxBackgroundColor = Color.Transparent
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFFAF9FB))
.background(appColors.background)
) {
when {
model.isLoading -> {
@@ -179,7 +204,8 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier.fillMaxSize()
) {
// 顶部背景区域(圆角在底部,覆盖状态栏)
val banner = model.profile?.banner
// 优先显示新选择的背景图,如果没有则显示原有的背景图
val banner = model.bannerImageUrl?.toString() ?: model.profile?.banner
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
Box(
modifier = Modifier
@@ -230,15 +256,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
) {
// 更换封面图标
Icon(
painter = painterResource(
id = if (AppState.darkMode) {
// TODO: 添加更换封面暗色模式图标
R.mipmap.frame_4 // 临时占位,需替换为实际图标
} else {
// TODO: 添加更换封面亮色模式图标
R.mipmap.fengm // 临时占位,需替换为实际图标
}
),
painter = painterResource(id = R.mipmap.fengm),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color.White
@@ -318,7 +336,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.border(2.4.dp, Color(0xFFFAF9FB), CircleShape),
.border(2.4.dp, appColors.background, CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
@@ -338,15 +356,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(
id = if (AppState.darkMode) {
// TODO: 添加编辑头像暗色模式图标
R.mipmap.frame_4 // 临时占位,需替换为实际图标
} else {
// TODO: 添加编辑头像亮色模式图标
R.mipmap.bi // 临时占位,需替换为实际图标
}
),
painter = painterResource(id = R.mipmap.bi),
contentDescription = "Edit Avatar",
modifier = Modifier.size(16.dp),
tint = Color.White
@@ -390,7 +400,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.background(appColors.secondaryBackground)
) {
// MBTI 类型
ProfileSelectItem(
@@ -411,7 +421,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier
.fillMaxWidth()
.height(0.3.dp)
.background(Color(0x41413C43).copy(alpha = 0.2f))
.background(appColors.divider)
.padding(horizontal = 16.dp)
)
@@ -453,10 +463,28 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
)
)
.debouncedClickable(
enabled = validate() && !model.isUpdating,
enabled = !model.isUpdating,
debounceTime = 1000L
) {
if (validate() && !model.isUpdating) {
if (model.isUpdating) return@debouncedClickable
// 点击保存时重新验证
val nicknameErrorMsg = validateNickname()
val bioErrorMsg = validateBio()
// 如果有错误,显示对应的错误提示
when {
nicknameErrorMsg != null -> {
Toast.makeText(context, nicknameErrorMsg, Toast.LENGTH_SHORT).show()
return@debouncedClickable
}
bioErrorMsg != null -> {
Toast.makeText(context, bioErrorMsg, Toast.LENGTH_SHORT).show()
return@debouncedClickable
}
}
// 验证通过,执行保存
model.viewModelScope.launch {
model.isUpdating = true
model.updateUserProfile(context)
@@ -467,7 +495,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
model.isUpdating = false
}
}
}
},
contentAlignment = Alignment.Center
) {
@@ -510,13 +537,12 @@ fun ProfileInfoCard(
isMultiline: Boolean = false
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp
.clip(RoundedCornerShape(16.dp))
.background(Color.White),
.background(appColors.secondaryBackground),
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
) {
Row(
@@ -531,7 +557,7 @@ fun ProfileInfoCard(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black,
color = appColors.text,
modifier = Modifier.width(100.dp)
)
@@ -546,7 +572,7 @@ fun ProfileInfoCard(
text = placeholder,
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0x993C3C43),
color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth()
)
}
@@ -558,9 +584,9 @@ fun ProfileInfoCard(
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
color = appColors.text
),
cursorBrush = SolidColor(Color.Black),
cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
singleLine = !isMultiline
)
@@ -581,6 +607,7 @@ fun ProfileSelectItem(
iconResDark: Int? = null,
iconResLight: Int? = null
) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
@@ -598,7 +625,8 @@ fun ProfileSelectItem(
Icon(
painter = painterResource(
id = if (AppState.darkMode) {
iconResDark ?: R.mipmap.frame_4 // 使用传入的暗色模式图标,或默认占位
// 暗色模式下使用和亮色模式一样的图标
iconResLight ?: iconResDark ?: R.mipmap.naoz
} else {
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
}
@@ -612,7 +640,7 @@ fun ProfileSelectItem(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
color = appColors.text
)
}
@@ -624,14 +652,14 @@ fun ProfileSelectItem(
text = value,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0x993C3C43)
color = appColors.secondaryText
)
Icon(
imageVector = Icons.Default.ArrowForward,
contentDescription = null,
modifier = Modifier.size(8.dp),
tint = Color(0x4D3C3C43)
tint = appColors.secondaryText
)
}
}

View File

@@ -10,6 +10,7 @@ import androidx.compose.animation.togetherWith
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
@@ -36,6 +37,13 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
)
}
) { targetCount ->
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp, color = AppColors.text)
Text(
text = "$targetCount",
modifier = modifier,
fontSize = fontSize.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -426,7 +426,6 @@ fun MomentOperateBtn(count: String, content: @Composable () -> Unit) {
fontSize = 14,
modifier = Modifier
.padding(start = 7.dp)
.width(24.dp)
)
}
}

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -131,17 +133,25 @@ fun FollowerListScreen(userId: Int) {
)
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text(
text = "还没有人关注你呢",
text = stringResource(R.string.follower_empty_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = "试着发信号出来,某人就会被吸引啦~",
text = stringResource(R.string.follower_empty_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -25,6 +25,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems
@@ -122,17 +124,25 @@ fun FollowerNoticeScreen() {
)
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
androidx.compose.material.Text(
text = "还没有人关注你呢",
text = stringResource(R.string.follower_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = "试着发信号出来,某人就会被吸引啦~",
text = stringResource(R.string.follower_empty_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -133,17 +135,25 @@ fun FollowingListScreen(userId: Int) {
)
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text(
text = "还没有关注任何灵魂",
text = stringResource(R.string.following_empty_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = "探索一下,总有一个你想靠近的光点 ✨",
text = stringResource(R.string.following_empty_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -13,9 +13,6 @@ import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo

View File

@@ -519,14 +519,31 @@ fun SideMenuContent(
var messageNotificationEnabled by remember { mutableStateOf(true) }
var darkModeEnabled by remember { mutableStateOf(AppState.darkMode) }
// 菜单背景色 #FAF9FB
val menuBackgroundColor = Color(0xFFFAF9FB)
// 同步暗色模式状态
LaunchedEffect(AppState.darkMode) {
darkModeEnabled = AppState.darkMode
}
// 菜单背景色 - 根据暗色模式适配
val menuBackgroundColor = if (darkModeEnabled) {
appColors.secondaryBackground // 暗色模式:深灰色
} else {
Color(0xFFFAF9FB) // 亮色模式:浅灰色
}
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 白色
val cardBackgroundColor = Color.White
// 跟随系统文字颜色 #979499
val followSystemTextColor = Color(0xFF979499)
// 卡片背景色 - 根据暗色模式适配
val cardBackgroundColor = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景
} else {
Color.White // 亮色模式:白色
}
// 文字颜色 - 根据暗色模式适配
val textColor = appColors.text
// 图标颜色 - 根据暗色模式适配
val iconColor = appColors.text
// 跟随系统文字颜色 - 根据暗色模式适配
val followSystemTextColor = appColors.secondaryText
// 开关开启颜色 #7C45ED
val switchActiveColor = Color(0xFF7C45ED)
@@ -579,14 +596,14 @@ fun SideMenuContent(
painter = painterResource(id = R.mipmap.sao),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.Black)
colorFilter = ColorFilter.tint(iconColor)
)
}
// 绝对定位的"扫一扫"文字上方71.5dp右侧66dp
Text(
text = stringResource(R.string.scan_qr),
fontSize = 14.sp,
color = Color.Black,
color = textColor,
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-66).dp, y = 91.5.dp)
@@ -602,7 +619,7 @@ fun SideMenuContent(
.noRippleClickable {
// TODO: 实现QR码功能
},
colorFilter = ColorFilter.tint(Color.Black)
colorFilter = ColorFilter.tint(iconColor)
)
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt绝对定位
@@ -616,6 +633,8 @@ fun SideMenuContent(
// 第一组卡片:编辑资料、账号安全、收藏
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 164.dp,
items = listOf(
@@ -655,6 +674,8 @@ fun SideMenuContent(
// 第二组卡片:暗色模式、消息通知
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 112.dp, // 根据设计图第二组卡片高度为112dp
items = listOf(
@@ -709,6 +730,8 @@ fun SideMenuContent(
// 第三组卡片:关于派派、反馈、退出登录
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 164.dp,
items = listOf(
@@ -776,6 +799,8 @@ data class MenuItem(
@Composable
fun MenuCard(
backgroundColor: Color,
textColor: Color,
iconColor: Color,
items: List<MenuItem>,
width: androidx.compose.ui.unit.Dp? = null,
height: androidx.compose.ui.unit.Dp? = null
@@ -794,14 +819,15 @@ fun MenuCard(
.then(if (height != null) Modifier.weight(1f) else Modifier),
contentAlignment = Alignment.Center
) {
MenuItemRow(item = item, compact = height != null) // 传递compact参数
MenuItemRow(item = item, compact = height != null, textColor = textColor, iconColor = iconColor) // 传递颜色参数
}
}
}
}
@Composable
fun MenuItemRow(item: MenuItem, compact: Boolean = false) {
fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, iconColor: Color) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
@@ -825,12 +851,12 @@ fun MenuItemRow(item: MenuItem, compact: Boolean = false) {
painter = painterResource(id = item.icon),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.Black)
colorFilter = ColorFilter.tint(iconColor)
)
Text(
text = item.label,
fontSize = 14.sp,
color = Color.Black
color = textColor
)
}
@@ -841,7 +867,7 @@ fun MenuItemRow(item: MenuItem, compact: Boolean = false) {
painter = painterResource(id = R.drawable.rave_now_nav_right),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color(0xFF111213))
colorFilter = ColorFilter.tint(appColors.text)
)
}
}

View File

@@ -50,6 +50,8 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -981,9 +983,13 @@ fun ChatRoomCard(
Text(
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp,
modifier = Modifier.alpha(0.6f),
modifier = Modifier
.alpha(0.6f)
.weight(1f),
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
fontWeight = androidx.compose.ui.text.font.FontWeight.W500,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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
@@ -108,13 +109,21 @@ fun AgentChatListScreen() {
text = stringResource(R.string.agent_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.agent_chat_empty_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
else {
@@ -130,13 +139,21 @@ fun AgentChatListScreen() {
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(

View File

@@ -186,13 +186,21 @@ fun AllChatListScreen() {
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_empty_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
} else {
Spacer(modifier = Modifier.height(39.dp))
@@ -207,13 +215,21 @@ fun AllChatListScreen() {
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(

View File

@@ -96,13 +96,21 @@ fun FriendChatListScreen() {
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_empty_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
@@ -117,13 +125,21 @@ fun FriendChatListScreen() {
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(

View File

@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -88,13 +89,21 @@ fun GroupChatListScreen() {
text = stringResource(R.string.group_chat_empty),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.group_chat_empty_join),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
@@ -109,13 +118,21 @@ fun GroupChatListScreen() {
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(

View File

@@ -59,6 +59,7 @@ import androidx.compose.ui.graphics.ColorFilter
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.UnderlineTabItem
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts.ShortVideoScreen
/**
* 动态列表
@@ -71,8 +72,8 @@ fun MomentsList() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 现在有6个tab推荐、短视频、新闻、探索、关注、热门
val tabCount = 6
// 根据登录状态设置标签页数量游客模式5个tab非游客模式6个tab
val tabCount = if (AppStore.isGuest) 5 else 6
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
Column(
@@ -173,14 +174,14 @@ fun MomentsList() {
}
)
} else {
// 热门标签 (游客模式)
// 热门标签 (游客模式) - 在游客模式下热门标签对应第3页
UnderlineTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 4,
isSelected = pagerState.currentPage == 3,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(4)
pagerState.animateScrollToPage(3)
}
}
}
@@ -188,14 +189,15 @@ fun MomentsList() {
}
// 新闻标签
// 新闻标签 - 在游客模式下对应第4页非游客模式下对应第5页
val newsPageIndex = if (AppStore.isGuest) 4 else 5
UnderlineTabItem(
text = stringResource(R.string.tab_news),
isSelected = pagerState.currentPage == 5,
isSelected = pagerState.currentPage == newsPageIndex,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(5)
pagerState.animateScrollToPage(newsPageIndex)
}
}
}
@@ -234,6 +236,7 @@ fun MomentsList() {
}
1 -> {
// 短视频页面
ShortVideoScreen()
}
2 -> {
// 动态页面 - 暂时显示时间线内容
@@ -248,17 +251,23 @@ fun MomentsList() {
}
}
4 -> {
// 热门页面 (仅非游客用户)
// 热门页面 (仅非游客用户) 或 新闻页面 (游客用户)
if (AppStore.isGuest) {
NewsScreen()
} else {
HotMomentsList()
}
}
5 -> {
// 新闻页面
// 新闻页面 (仅非游客用户)
if (!AppStore.isGuest) {
NewsScreen()
}
}
}
}
}
}
@Composable
fun CustomTabItem(
text: String,

View File

@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
@@ -137,8 +139,18 @@ fun DiscoverView() {
val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent
// 对于英文和日文,每行字符数会更少,使用更保守的估算
val estimatedCharsPerLine = if (textContent.isNotEmpty()) {
// 检测是否包含非中文字符(英文、日文等)
val hasNonChinese = textContent.any {
val code = it.code
!(code >= 0x4E00 && code <= 0x9FFF) // 不在中文字符范围内
}
if (hasNonChinese) 15 else 20 // 英文/日文每行更少字符
} else {
20
}
val textLines = if (textContent.isNotEmpty()) {
val estimatedCharsPerLine = 20
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行
} else {
@@ -146,8 +158,8 @@ fun DiscoverView() {
}
val baseHeight = 200.dp
val singleLineTextHeight = 20.dp
val doubleLineTextHeight = 40.dp
val singleLineTextHeight = 24.dp // 增加高度以适应英文/日文
val doubleLineTextHeight = 44.dp // 增加高度以适应英文/日文
val authorInfoHeight = 25.dp
val paddingHeight = 10.dp
val paddingHeight2 = 3.dp
@@ -160,7 +172,6 @@ fun DiscoverView() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight)
.padding(2.dp)
.noRippleClickable {
debouncer {
@@ -173,7 +184,9 @@ fun DiscoverView() {
}
) {
Column(
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
modifier = Modifier
.fillMaxWidth()
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
@@ -193,9 +206,9 @@ fun DiscoverView() {
Column(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight - baseHeight)
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
// 文本内容区域,限制最大高度
if (momentItem.momentTextContent.isNotEmpty()) {
androidx.compose.material3.Text(
text = momentItem.momentTextContent,
@@ -203,13 +216,19 @@ fun DiscoverView() {
fontSize = 12.sp,
color = AppColors.text,
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
lineHeight = 16.sp // 设置行高以适应不同语言
)
}
// 使用 Spacer 确保头像昵称栏始终在底部,有足够的空间
Spacer(modifier = Modifier.weight(1f))
// 头像昵称栏,确保始终完整显示,设置最小高度避免被挤压
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 25.dp) // 最小高度确保完整显示,自适应避免被挤压
.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -225,7 +244,9 @@ fun DiscoverView() {
androidx.compose.material3.Text(
text = momentItem.nickname,
modifier = Modifier.padding(start = 4.dp),
modifier = Modifier
.padding(start = 4.dp)
.weight(1f),
fontSize = 11.sp,
color = AppColors.text.copy(alpha = 0.6f),
maxLines = 1,

View File

@@ -0,0 +1,185 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.index.tabs.shorts.ShortViewCompose
import kotlinx.coroutines.launch
/**
* 短视频页面
*/
@Composable
fun ShortVideoScreen() {
val viewModel = ShortVideoViewModel
val allMoments = viewModel.moments
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current
val momentLoader = viewModel.momentLoader
// 记录当前播放的短视频索引,切换 Tab 返回时恢复
val currentIndex = rememberSaveable { androidx.compose.runtime.mutableStateOf(0) }
// 过滤出包含视频的动态
val videoMoments = remember(allMoments) {
val filtered = allMoments.filter { it.videos != null && it.videos.isNotEmpty() }
Log.d("ShortVideoScreen", "过滤视频动态 - 总动态数: ${allMoments.size}, 包含视频的动态数: ${filtered.size}")
filtered.forEach { moment ->
Log.d("ShortVideoScreen", "视频动态 ID: ${moment.id}, 视频数: ${moment.videos?.size}, 第一个视频URL: ${moment.videos?.firstOrNull()?.url}")
}
filtered
}
// 初始加载数据
LaunchedEffect(Unit) {
Log.d("ShortVideoScreen", "开始加载数据")
viewModel.refreshPager()
}
// 加载更多数据
LaunchedEffect(allMoments.size, videoMoments.size) {
Log.d("ShortVideoScreen", "检查是否需要加载更多 - allMoments: ${allMoments.size}, videoMoments: ${videoMoments.size}, hasNext: ${momentLoader.hasNext}, isLoading: ${momentLoader.isLoading}")
if (allMoments.isNotEmpty() && videoMoments.size < 10 && momentLoader.hasNext && !momentLoader.isLoading) {
// 如果视频数量少于10个尝试加载更多
Log.d("ShortVideoScreen", "开始加载更多数据")
viewModel.loadMore()
}
}
// 加载状态
if (momentLoader.isLoading && videoMoments.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center
) {
CircularProgressIndicator(color = AppColors.main)
Text(
text = "加载中...",
modifier = Modifier.padding(top = 16.dp),
color = AppColors.text,
fontSize = 14.sp
)
}
}
}
// 错误状态
else if (momentLoader.error != null && videoMoments.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background),
contentAlignment = Alignment.Center
) {
Text(
text = "加载失败: ${momentLoader.error}",
color = AppColors.error,
fontSize = 14.sp
)
}
}
// 空状态 - 已加载但无视频
else if (!momentLoader.isLoading && videoMoments.isEmpty() && allMoments.isNotEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background),
contentAlignment = Alignment.Center
) {
Text(
text = "暂无短视频\n已加载 ${allMoments.size} 条动态,但都不包含视频",
color = AppColors.text,
fontSize = 16.sp
)
}
}
// 初始状态 - 还没有加载过数据
else if (!momentLoader.isLoading && videoMoments.isEmpty() && allMoments.isEmpty() && momentLoader.error == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background),
contentAlignment = Alignment.Center
) {
Text(
text = "准备加载...",
color = AppColors.text,
fontSize = 16.sp
)
}
}
// 显示视频列表
else if (videoMoments.isNotEmpty()) {
ShortViewCompose(
videoMoments = videoMoments,
clickItemPosition = currentIndex.value,
onLikeClick = { moment ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (moment.liked) {
viewModel.dislikeMoment(moment.id)
} else {
viewModel.likeMoment(moment.id)
}
}
}
},
onCommentClick = { moment ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
viewModel.onAddComment(moment.id)
}
}
},
onFavoriteClick = { moment ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (moment.isFavorite) {
viewModel.unfavoriteMoment(moment.id)
} else {
viewModel.favoriteMoment(moment.id)
}
}
}
},
onShareClick = { moment ->
// TODO: 实现分享功能
},
onPageChanged = { idx -> currentIndex.value = idx }
)
}
}

View File

@@ -0,0 +1,21 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
import org.greenrobot.eventbus.EventBus
object ShortVideoViewModel : BaseMomentModel() {
init {
EventBus.getDefault().register(this)
}
override fun extraArgs(): MomentLoaderExtraArgs {
return MomentLoaderExtraArgs(explore = true, videoOnly = true)
}
// 获取包含视频的动态列表
fun getVideoMoments(): List<com.aiosman.ravenow.entity.MomentEntity> {
return moments.filter { it.videos != null && it.videos.isNotEmpty() }
}
}

View File

@@ -604,9 +604,19 @@ fun TopNavigationBar(
val appColors = LocalAppTheme.current
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
// 根据背景透明度决定图标颜色透明度为1时变黑否则为白色
val iconColor = if (backgroundAlpha >= 1f) Color.Black else Color.White
val cardBorderColor = if (backgroundAlpha >= 1f) Color.Black else Color.White
// 根据背景透明度和暗色模式决定图标颜色
// 暗色模式下:图标始终为白色
// 亮色模式下根据背景透明度决定透明度为1时变黑否则为白色
val iconColor = if (AppState.darkMode) {
Color.White // 暗色模式下图标始终为白色
} else {
if (backgroundAlpha >= 1f) Color.Black else Color.White
}
val cardBorderColor = if (AppState.darkMode) {
Color.White // 暗色模式下边框应为白色
} else {
if (backgroundAlpha >= 1f) Color.Black else Color.White
}
Box(
modifier = Modifier
@@ -625,7 +635,12 @@ fun TopNavigationBar(
val baseColor = remember(backgroundAlpha) {
val smoothProgress = backgroundAlpha.coerceIn(0f, 1f)
// 初始状态:半透明深色,让白色图标清晰可见
if (AppState.darkMode) {
// 暗色模式下从黑色透明度0逐渐变为黑色透明度1
val alpha = smoothProgress.coerceIn(0f, 1f)
Color.Black.copy(alpha = alpha)
} else {
// 亮色模式:初始状态:半透明深色,让白色图标清晰可见
val initialDarkAlpha = 0.12f
// 使用平滑的插值函数,让整个过渡更自然
@@ -645,6 +660,7 @@ fun TopNavigationBar(
alpha = alpha.coerceIn(0f, initialDarkAlpha)
)
}
}
Box(
modifier = Modifier
@@ -703,7 +719,7 @@ fun TopNavigationBar(
text = numberFormat.format(interactionCount),
fontSize = 14.sp,
fontWeight = FontWeight.W500,
color = Color.Black, // 文字始终为黑色
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
textAlign = TextAlign.Center
)
}
@@ -744,6 +760,9 @@ fun TopNavigationBar(
// 如果不是主页面,显示返回按钮和用户信息
if (!isMain) {
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
// 判断是否有背景图
val hasBanner = profile?.banner != null
Row(
modifier = Modifier
.align(Alignment.TopStart)
@@ -751,6 +770,8 @@ fun TopNavigationBar(
.padding(top = statusBarPadding.calculateTopPadding()),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮:深色模式下为白色,亮色模式下为黑色
val backButtonColor = if (AppState.darkMode) Color.White else Color.Black
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
@@ -759,29 +780,23 @@ fun TopNavigationBar(
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
colorFilter = ColorFilter.tint(backButtonColor) // 深色模式下为白色,亮色模式下为黑色
)
// 未设置背景的用户(自己的主页且没有背景图)显示昵称
if (isSelf && !hasBanner && profile != null) {
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
text = profile.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.White
color = backButtonColor // 深色模式下为白色,亮色模式下为黑色
)
}
}
}
}
}
// 分享图标(向上箭头)
@Composable

View File

@@ -42,6 +42,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
@@ -202,7 +204,11 @@ fun GalleryGrid(
text = stringResource(R.string.your_story_not_started),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
@@ -211,7 +217,11 @@ fun GalleryGrid(
text = stringResource(R.string.publish_moment_greeting),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
} else {
@@ -221,7 +231,7 @@ fun GalleryGrid(
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
) {
itemsIndexed(moments) { idx, moment ->
if (moment != null) {
if (moment != null && moment.images.isNotEmpty()) {
val itemDebouncer = rememberDebouncer()
Box(
modifier = Modifier

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -29,26 +28,23 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
@Composable
fun GroupChatEmptyContent() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
// 分割线(紧贴上方栏)
Divider(
color = Color(0xFFF0F0F0), // 更浅的灰色
thickness = 1.dp,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
@@ -75,7 +71,11 @@ fun GroupChatEmptyContent() {
text = stringResource(R.string.empty_nothing),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF000000)
color = AppColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
@@ -87,6 +87,7 @@ private fun SegmentedControl(
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
@@ -98,7 +99,8 @@ private fun SegmentedControl(
text = stringResource(R.string.chat_all),
isSelected = selectedIndex == 0,
onClick = { onSegmentSelected(0) },
width = 54.dp
width = 54.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
@@ -108,7 +110,8 @@ private fun SegmentedControl(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp
width = 59.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
@@ -118,7 +121,8 @@ private fun SegmentedControl(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp
width = 54.dp,
appColors = AppColors
)
}
}
@@ -128,7 +132,8 @@ private fun SegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp
width: androidx.compose.ui.unit.Dp,
appColors: com.aiosman.ravenow.AppThemeData
) {
Box(
modifier = Modifier
@@ -136,7 +141,7 @@ private fun SegmentButton(
.height(32.dp)
.background(
color = if (isSelected) {
Color(0xFF110C13) // RGB(17, 12, 19)
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
} else {
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
},
@@ -149,7 +154,11 @@ private fun SegmentButton(
text = text,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) Color(0xFFFFFFFF) else Color(0xFF000000)
color = if (isSelected) {
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
} else {
appColors.text // 未选中时使用文本颜色
}
)
}
}

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -35,6 +34,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -209,13 +209,6 @@ fun AgentEmptyContentWithSegments() {
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
// 分割线(紧贴上方栏)
Divider(
color = Color(0xFFF0F0F0), // 更浅的灰色
thickness = 1.dp,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
@@ -250,7 +243,11 @@ fun AgentEmptyContentWithSegments() {
text = stringResource(R.string.exclusive_ai_waiting),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
@@ -259,7 +256,11 @@ fun AgentEmptyContentWithSegments() {
text = stringResource(R.string.ai_companion_not_tool),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
} else {
Image(
@@ -303,6 +304,7 @@ private fun AgentSegmentedControl(
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
@@ -314,7 +316,8 @@ private fun AgentSegmentedControl(
text = stringResource(R.string.chat_all),
isSelected = selectedIndex == 0,
onClick = { onSegmentSelected(0) },
width = 54.dp
width = 54.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
@@ -324,7 +327,8 @@ private fun AgentSegmentedControl(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp
width = 59.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
@@ -334,7 +338,8 @@ private fun AgentSegmentedControl(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp
width = 54.dp,
appColors = AppColors
)
}
}
@@ -344,7 +349,8 @@ private fun AgentSegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp
width: androidx.compose.ui.unit.Dp,
appColors: com.aiosman.ravenow.AppThemeData
) {
Box(
modifier = Modifier
@@ -352,7 +358,7 @@ private fun AgentSegmentButton(
.height(32.dp)
.background(
color = if (isSelected) {
Color(0xFF110C13) // RGB(17, 12, 19)
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
} else {
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
},
@@ -365,7 +371,11 @@ private fun AgentSegmentButton(
text = text,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) Color(0xFFFFFFFF) else Color(0xFF000000)
color = if (isSelected) {
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
} else {
appColors.text // 未选中时使用文本颜色
}
)
}
}

View File

@@ -61,7 +61,7 @@ fun UserContentPageIndicator(
text = stringResource(R.string.index_dynamic),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 0) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 0) Color(0xFF000000) else Color(0x993C3C43)
color = if (pagerState.currentPage == 0) AppColors.text else AppColors.secondaryText
)
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 0) 6.dp else 4.dp))
if (pagerState.currentPage == 0) {
@@ -69,7 +69,7 @@ fun UserContentPageIndicator(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(Color(0xFF000000))
.background(AppColors.text)
)
}
}
@@ -91,7 +91,7 @@ fun UserContentPageIndicator(
text = stringResource(R.string.chat_ai),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 1) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 1) Color(0xFF000000) else Color(0x993C3C43)
color = if (pagerState.currentPage == 1) AppColors.text else AppColors.secondaryText
)
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 1) 6.dp else 4.dp))
if (pagerState.currentPage == 1) {
@@ -99,7 +99,7 @@ fun UserContentPageIndicator(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(Color(0xFF000000))
.background(AppColors.text)
)
}
}
@@ -122,7 +122,7 @@ fun UserContentPageIndicator(
text = stringResource(R.string.chat_group),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 2) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 2) Color(0xFF000000) else Color(0x993C3C43)
color = if (pagerState.currentPage == 2) AppColors.text else AppColors.secondaryText
)
Spacer(modifier = Modifier.height(if (pagerState.currentPage == 2) 6.dp else 4.dp))
if (pagerState.currentPage == 2) {
@@ -130,7 +130,7 @@ fun UserContentPageIndicator(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(Color(0xFF000000))
.background(AppColors.text)
)
}
}

View File

@@ -118,7 +118,7 @@ fun UserItem(
text = postCount.toString(),
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(2.dp))
@@ -126,7 +126,7 @@ fun UserItem(
text = stringResource(R.string.posts),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -153,7 +153,7 @@ fun UserItem(
text = formattedFollowerCount,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(2.dp))
@@ -161,7 +161,7 @@ fun UserItem(
text = stringResource(R.string.followers_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -189,7 +189,7 @@ fun UserItem(
text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(2.dp))
@@ -197,7 +197,7 @@ fun UserItem(
text = stringResource(R.string.following_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -217,7 +217,7 @@ fun UserItem(
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
letterSpacing = (-0.3).sp,
color = Color(0xFF000000)
color = AppColors.text
)
Spacer(modifier = Modifier.height(4.dp))
@@ -227,7 +227,7 @@ fun UserItem(
Text(
text = accountProfileEntity.bio,
fontSize = 13.sp,
color = Color(0x993C3C43), // 60/255, 60/255, 67/255, alpha 0.6
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -235,7 +235,7 @@ fun UserItem(
Text(
text = "Welcome to my fantiac word i will show you something about magic",
fontSize = 13.sp,
color = Color(0x993C3C43),
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -258,7 +258,7 @@ fun UserItem(
ProfileTag(
text = mbti,
backgroundColor = Color(0x33FF8D28), // 255/255, 141/255, 40/255, alpha 0.2
textColor = Color(0xFF000000)
textColor = AppColors.text
)
}
@@ -267,7 +267,7 @@ fun UserItem(
ProfileTag(
text = zodiac,
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
textColor = Color(0xFF000000)
textColor = AppColors.text
)
}
@@ -276,10 +276,10 @@ fun UserItem(
ProfileTag(
text = stringResource(R.string.edit_profile),
backgroundColor = Color(0x14947A80), // 124/255, 116/255, 128/255, alpha 0.08
textColor = Color(0xFF9284BD), // 146/255, 132/255, 189/255
textColor = AppColors.text,
leadingIcon = {
EditIcon(
color = Color(0xFF9284BD),
color = AppColors.text,
modifier = Modifier.size(16.dp)
)
},

View File

@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -61,27 +62,65 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun ShortViewCompose(
videoItemsUrl: List<String>,
videoItemsUrl: List<String> = emptyList(),
videoMoments: List<MomentEntity> = emptyList(),
clickItemPosition: Int = 0,
videoHeader: @Composable () -> Unit = {},
videoBottom: @Composable () -> Unit = {}
videoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
onPageChanged: ((Int) -> Unit)? = null
) {
val pagerState: PagerState = run {
remember {
PagerState(clickItemPosition, 0, videoItemsUrl.size - 1)
// 优先使用 videoMoments如果没有则使用 videoItemsUrl
val items = if (videoMoments.isNotEmpty()) {
videoMoments.mapNotNull { moment ->
// MomentVideoEntity 的 url 已经在 toMomentItem() 中添加了 BASE_SERVER 前缀
moment.videos?.firstOrNull()?.url
}
} else {
videoItemsUrl
}
// 如果视频列表为空,显示空状态
if (items.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "暂无视频",
color = Color.White,
fontSize = 16.sp
)
}
return
}
// 确保 items 不为空后再创建 PagerState
val pagerState: PagerState = remember(items.size) {
val maxPage = maxOf(0, items.size - 1)
PagerState(
currentPage = clickItemPosition.coerceIn(0, maxPage),
minPage = 0,
maxPage = maxPage
)
}
val initialLayout = remember {
mutableStateOf(true)
@@ -89,20 +128,39 @@ fun ShortViewCompose(
val pauseIconVisibleState = remember {
mutableStateOf(false)
}
Pager(
modifier = Modifier
.fillMaxSize()
.clip(RectangleShape),
state = pagerState,
orientation = Orientation.Vertical,
offscreenLimit = 1
) {
pauseIconVisibleState.value = false
val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) {
videoMoments[page]
} else {
null
}
// 同步页码到外部(用于返回时恢复进度)
LaunchedEffect(pagerState.currentPage) {
onPageChanged?.invoke(pagerState.currentPage)
}
SingleVideoItemContent(
videoItemsUrl[page],
pagerState,
page,
initialLayout,
pauseIconVisibleState,
videoHeader,
videoBottom
videoUrl = items[page],
moment = currentMoment,
pagerState = pagerState,
pager = page,
initialLayout = initialLayout,
pauseIconVisibleState = pauseIconVisibleState,
VideoHeader = videoHeader,
VideoBottom = videoBottom,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onFavoriteClick = onFavoriteClick,
onShareClick = onShareClick
)
}
@@ -116,18 +174,39 @@ fun ShortViewCompose(
@Composable
private fun SingleVideoItemContent(
videoUrl: String,
moment: MomentEntity?,
pagerState: PagerState,
pager: Int,
initialLayout: MutableState<Boolean>,
pauseIconVisibleState: MutableState<Boolean>,
VideoHeader: @Composable() () -> Unit,
VideoBottom: @Composable() () -> Unit,
VideoHeader: @Composable() () -> Unit = {},
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null
) {
Box(modifier = Modifier.fillMaxSize()) {
VideoPlayer(videoUrl, pagerState, pager, pauseIconVisibleState)
Box(
modifier = Modifier
.fillMaxSize()
.clip(RectangleShape) // 确保内容不会溢出到box外
) {
VideoPlayer(
videoUrl = videoUrl,
moment = moment,
pagerState = pagerState,
pager = pager,
pauseIconVisibleState = pauseIconVisibleState,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onFavoriteClick = onFavoriteClick,
onShareClick = onShareClick
)
VideoHeader.invoke()
if (moment != null && VideoBottom != null) {
Box(modifier = Modifier.align(Alignment.BottomStart)) {
VideoBottom.invoke()
VideoBottom.invoke(moment)
}
}
if (initialLayout.value) {
Box(
@@ -143,9 +222,14 @@ private fun SingleVideoItemContent(
@Composable
fun VideoPlayer(
videoUrl: String,
moment: MomentEntity?,
pagerState: PagerState,
pager: Int,
pauseIconVisibleState: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -158,9 +242,20 @@ fun VideoPlayer(
ExoPlayer.Builder(context)
.build()
.apply {
// 创建带有认证头的 HttpDataSource.Factory
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
.setUserAgent(Util.getUserAgent(context, context.packageName))
.setDefaultRequestProperties(
mapOf(
"Authorization" to "Bearer ${com.aiosman.ravenow.AppStore.token ?: ""}",
"DEVICE-OS" to "Android"
)
)
// 创建 DataSource.Factory使用自定义的 HttpDataSource.Factory
val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.packageName)
httpDataSourceFactory
)
val source = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)))
@@ -275,21 +370,54 @@ fun VideoPlayer(
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (moment != null) {
UserAvatar(avatarUrl = moment.avatar)
VideoBtn(
icon = R.drawable.rider_pro_video_like,
text = formatCount(moment.likeCount)
) {
moment?.let { onLikeClick?.invoke(it) }
}
VideoBtn(
icon = R.drawable.rider_pro_video_comment,
text = formatCount(moment.commentCount)
) {
moment?.let {
showCommentModal = true
onCommentClick?.invoke(it)
}
}
VideoBtn(
icon = R.drawable.rider_pro_video_favor,
text = formatCount(moment.favoriteCount)
) {
moment?.let { onFavoriteClick?.invoke(it) }
}
VideoBtn(
icon = R.drawable.rider_pro_video_share,
text = formatCount(moment.shareCount)
) {
moment?.let { onShareClick?.invoke(it) }
}
} else {
UserAvatar()
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k")
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") {
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "0")
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "0") {
showCommentModal = true
}
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "234")
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "677k")
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "0")
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "0")
}
}
}
// info
if (moment != null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomStart
) {
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
if (moment.location.isNotEmpty() && moment.location != "Worldwide") {
Row(
modifier = Modifier
.padding(bottom = 8.dp)
@@ -306,39 +434,43 @@ fun VideoPlayer(
)
Text(
modifier = Modifier.padding(end = 4.dp),
text = "USA",
text = moment.location,
fontSize = 12.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
Text(
text = "@Kevinlinpr",
text = "@${moment.nickname}",
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
if (moment.momentTextContent.isNotEmpty()) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp), // 确保Text占用可用宽度
text = "Pedro Acosta to join KTM in 2025 on a multi-year deal! \uD83D\uDFE0",
.padding(top = 4.dp),
text = moment.momentTextContent,
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),
overflow = TextOverflow.Ellipsis, // 超出范围时显示省略号
maxLines = 2 // 最多显示两行
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
}
}
}
}
if (showCommentModal) {
if (showCommentModal && moment != null) {
ModalBottomSheet(
onDismissRequest = { showCommentModal = false },
containerColor = Color.White,
sheetState = sheetState
) {
CommentModalContent() {
CommentModalContent(postId = moment.id) {
}
}
@@ -346,16 +478,37 @@ fun VideoPlayer(
}
@Composable
fun UserAvatar() {
Image(
fun UserAvatar(avatarUrl: String? = null) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
.size(40.dp)
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
.clip(
RoundedCornerShape(40.dp)
), painter = painterResource(id = R.drawable.default_avatar), contentDescription = ""
.clip(RoundedCornerShape(40.dp))
) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage(
imageUrl = avatarUrl,
contentDescription = "用户头像",
modifier = Modifier.fillMaxSize(),
defaultRes = R.drawable.default_avatar
)
} else {
Image(
painter = painterResource(id = R.drawable.default_avatar),
contentDescription = "用户头像"
)
}
}
}
// 格式化数字显示
private fun formatCount(count: Int): String {
return when {
count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
else -> count.toString()
}
}
@Composable

View File

@@ -204,7 +204,7 @@ fun EmailSignupScreen() {
email = it
},
label = stringResource(R.string.login_email_label),
hint = "输入电子邮件",
hint = stringResource(R.string.text_hint_email),
error = emailError,
leadingIcon = {
Image(

View File

@@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -68,6 +70,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -160,8 +163,9 @@ fun NewPostScreen() {
Row(
modifier = Modifier
.padding(start = 16.dp)
.width(100.dp)
.height(40.dp)
.widthIn(min = 100.dp, max = 200.dp)
.wrapContentWidth()
.clip(RoundedCornerShape(20.dp))
.background(
brush = Brush.linearGradient(
@@ -189,6 +193,8 @@ fun NewPostScreen() {
modifier = Modifier
.padding(start = 2.dp),
color = Color.White,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}

View File

@@ -149,7 +149,7 @@
<string name="agent_desc_hint">例:経験豊富な営業担当者で、ユーモアと生きた事例を使って、複雑な製品を顧客が理解しやすい形に変えるのが得意</string>
<string name="agent_create">エージェントを作成</string>
<string name="moment_content_hint">投稿のインスピレーションが必要ですかAIがお手伝いします</string>
<string name="moment_ai_co">AIの文案最適化</string>
<string name="moment_ai_co">文案最適化</string>
<string name="moment_ai_delete">削除</string>
<string name="moment_ai_apply">適用</string>
<string name="chat_ai">AI</string>
@@ -180,6 +180,10 @@
<string name="group_chat_empty_subtitle">ホームで興味のあるテーマルームを探してみましょう</string>
<string name="friend_chat_empty_title">まだ友達とチャットしていません~</string>
<string name="friend_chat_empty_subtitle">友達のアバターをクリックして、すぐにチャットを始めましょう。</string>
<string name="following_empty_title">まだ誰もフォローしていません</string>
<string name="following_empty_subtitle">探索してみてください。近づきたい光が見つかります ✨</string>
<string name="follower_empty_title">まだ誰もあなたをフォローしていません</string>
<string name="follower_empty_subtitle">信号を送ってみてください。誰かが引き寄せられるでしょう~</string>
<string name="friend_chat_me_prefix">私: </string>
<string name="friend_chat_load_failed">読み込みに失敗しました</string>
<string name="create_group_chat">グループチャットを作成</string>

View File

@@ -183,6 +183,10 @@
<string name="group_chat_empty_join">去首页探索感兴趣的高能对话</string>
<string name="friend_chat_empty_title">和朋友,还没有对话哦~</string>
<string name="friend_chat_empty_subtitle">点击好友头像,即刻发起聊天</string>
<string name="following_empty_title">还没有关注任何灵魂</string>
<string name="following_empty_subtitle">探索一下,总有一个你想靠近的光点 ✨</string>
<string name="follower_empty_title">还没有人关注你呢</string>
<string name="follower_empty_subtitle">试着发信号出来,某人就会被吸引啦~</string>
<string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">创建群聊</string>

View File

@@ -148,7 +148,7 @@
<string name="agent_desc_hint">Example: An experienced salesperson who is good at transforming complex products into topics that customers can easily understand and be interested in through humorous language and vivid cases</string>
<string name="agent_create">Create Agent</string>
<string name="moment_content_hint">Need some inspiration for your post? Let AI assist you!</string>
<string name="moment_ai_co">AI copywriting optimization</string>
<string name="moment_ai_co">copywriting optimization</string>
<string name="moment_ai_delete">Delete</string>
<string name="moment_ai_apply">Apply</string>
<string name="chat_ai">Ai</string>
@@ -179,6 +179,10 @@
<string name="group_chat_empty_subtitle">Explore interesting theme rooms on the homepage</string>
<string name="friend_chat_empty_title">Have not chatted with friends yet~</string>
<string name="friend_chat_empty_subtitle">Click on the avatar of friend to start chatting instantly.</string>
<string name="following_empty_title">Not following anyone yet</string>
<string name="following_empty_subtitle">Explore and you\'ll find a light you want to get close to ✨</string>
<string name="follower_empty_title">No one is following you yet</string>
<string name="follower_empty_subtitle">Try sending out a signal, someone will be attracted to you~</string>
<string name="friend_chat_me_prefix">Me: </string>
<string name="friend_chat_load_failed">Failed to load</string>
<string name="create_group_chat">Create Group Chat</string>