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
35 changed files with 1329 additions and 377 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

@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
@@ -80,6 +81,10 @@ fun MbtiSelectScreen() {
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
navController.navigateUp()
}
)

View File

@@ -29,24 +29,55 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// 星座列表
val ZODIAC_SIGNS = listOf(
"白羊座", "金牛座", "双子座", "巨蟹座",
"狮子座", "处女座", "天秤座", "天蝎座",
"射手座", "摩羯座", "水瓶座", "双鱼座"
// 星座资源ID列表
val ZODIAC_SIGN_RES_IDS = listOf(
R.string.zodiac_aries,
R.string.zodiac_taurus,
R.string.zodiac_gemini,
R.string.zodiac_cancer,
R.string.zodiac_leo,
R.string.zodiac_virgo,
R.string.zodiac_libra,
R.string.zodiac_scorpio,
R.string.zodiac_sagittarius,
R.string.zodiac_capricorn,
R.string.zodiac_aquarius,
R.string.zodiac_pisces
)
/**
* 根据存储的星座字符串可能是任何语言找到对应的资源ID
* 如果找不到返回null
*/
@Composable
fun findZodiacResId(storedZodiac: String?): Int? {
if (storedZodiac.isNullOrEmpty()) return null
// 尝试在所有语言的资源中查找匹配
ZODIAC_SIGN_RES_IDS.forEachIndexed { index, resId ->
val zodiacText = stringResource(resId)
if (zodiacText == storedZodiac) {
return resId
}
}
// 如果找不到精确匹配尝试通过资源ID索引查找兼容旧数据
// 这里可以根据需要添加更多兼容逻辑
return null
}
@Composable
fun ZodiacSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentZodiac = model.zodiac
val currentZodiacResId = findZodiacResId(model.zodiac)
Column(
modifier = Modifier
@@ -70,12 +101,20 @@ fun ZodiacSelectScreen() {
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(ZODIAC_SIGNS) { zodiac ->
items(ZODIAC_SIGN_RES_IDS.size) { index ->
val zodiacResId = ZODIAC_SIGN_RES_IDS[index]
val zodiacText = stringResource(zodiacResId)
ZodiacItem(
zodiac = zodiac,
isSelected = zodiac == currentZodiac,
zodiac = zodiacText,
zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId,
onClick = {
model.zodiac = zodiac
// 保存当前语言的星座文本
model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
navController.navigateUp()
}
)
@@ -88,6 +127,7 @@ fun ZodiacSelectScreen() {
@Composable
fun ZodiacItem(
zodiac: String,
zodiacResId: Int,
isSelected: Boolean,
onClick: () -> Unit
) {

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,10 +109,21 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.name = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
usernameError = when {
cleanValue.trim().isEmpty() -> "昵称不能为空"
cleanValue.length < 3 -> "昵称长度不能小于3"
cleanValue.length > 20 -> "昵称长度不能大于20"
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
}
}
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
}
}
@@ -118,8 +134,17 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 去除换行符,确保个人简介不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.bio = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
bioError = when {
cleanValue.length > 100 -> "个人简介长度不能大于100"
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
}
}
@@ -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,21 +256,13 @@ 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
)
Text(
text = "更换封面",
text = stringResource(R.string.change_cover),
fontSize = 12.sp,
color = Color.White
)
@@ -288,7 +306,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 标题
Text(
text = "编辑资料",
text = stringResource(R.string.edit_profile_info),
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = 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
@@ -365,7 +375,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
) {
// 昵称输入框
ProfileInfoCard(
label = "昵称",
label = stringResource(R.string.nickname),
value = model.name,
placeholder = "Value",
onValueChange = { onNicknameChange(it) },
@@ -376,7 +386,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 个人简介输入框
ProfileInfoCard(
label = "个人简介",
label = stringResource(R.string.personal_intro),
value = model.bio,
placeholder = "Welcome to my fantiac word i will show you something about magic",
onValueChange = { onBioChange(it) },
@@ -390,11 +400,11 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.background(appColors.secondaryBackground)
) {
// MBTI 类型
ProfileSelectItem(
label = "MBTI 类型",
label = stringResource(R.string.mbti_type),
value = model.mbti ?: "ENFP",
iconColor = Color(0xFF7C45ED),
iconResDark = null, // TODO: 添加MBTI暗色模式图标
@@ -411,14 +421,19 @@ 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)
)
// 星座(使用当前图标)
ProfileSelectItem(
label = "星座",
value = model.zodiac ?: "白羊座",
label = stringResource(R.string.zodiac),
value = model.zodiac?.let { storedZodiac ->
// 尝试找到对应的资源ID并显示当前语言的文本
findZodiacResId(storedZodiac)?.let { resId ->
stringResource(resId)
} ?: storedZodiac // 如果找不到,显示原始存储的值
} ?: stringResource(R.string.zodiac_aries),
iconColor = Color(0xFFFFCC00),
iconResDark = R.mipmap.frame_4, // 星座暗色模式图标
iconResLight = R.mipmap.xingzuo, // 星座亮色模式图标
@@ -448,26 +463,43 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
)
)
.debouncedClickable(
enabled = validate() && !model.isUpdating,
enabled = !model.isUpdating,
debounceTime = 1000L
) {
if (validate() && !model.isUpdating) {
model.viewModelScope.launch {
model.isUpdating = true
model.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
navController.navigateUp()
}
model.isUpdating = false
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)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
navController.navigateUp()
}
model.isUpdating = false
}
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
text = stringResource(R.string.save),
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.White
@@ -483,7 +515,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
contentAlignment = Alignment.Center
) {
Text(
text = "加载用户资料失败,请重试",
text = stringResource(R.string.error_load_profile_failed),
color = appColors.text
)
}
@@ -505,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(
@@ -526,7 +557,7 @@ fun ProfileInfoCard(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black,
color = appColors.text,
modifier = Modifier.width(100.dp)
)
@@ -541,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()
)
}
@@ -553,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
)
@@ -576,6 +607,7 @@ fun ProfileSelectItem(
iconResDark: Int? = null,
iconResLight: Int? = null
) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
@@ -593,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 // 使用传入的亮色模式图标,或默认占位
}
@@ -607,7 +640,7 @@ fun ProfileSelectItem(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
color = appColors.text
)
}
@@ -619,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,12 +251,18 @@ fun MomentsList() {
}
}
4 -> {
// 热门页面 (仅非游客用户)
HotMomentsList()
// 热门页面 (仅非游客用户) 或 新闻页面 (游客用户)
if (AppStore.isGuest) {
NewsScreen()
} else {
HotMomentsList()
}
}
5 -> {
// 新闻页面
NewsScreen()
// 新闻页面 (仅非游客用户)
if (!AppStore.isGuest) {
NewsScreen()
}
}
}
}

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,21 +158,20 @@ 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
val paddingHeight2 = 3.dp
val totalHeight = baseHeight + when (textLines) {
0 -> authorInfoHeight + paddingHeight
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
else -> doubleLineTextHeight + authorInfoHeight +paddingHeight2
else -> doubleLineTextHeight + authorInfoHeight + paddingHeight2
}
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,25 +635,31 @@ fun TopNavigationBar(
val baseColor = remember(backgroundAlpha) {
val smoothProgress = backgroundAlpha.coerceIn(0f, 1f)
// 初始状态:半透明深色,让白色图标清晰可见
val initialDarkAlpha = 0.12f
if (AppState.darkMode) {
// 暗色模式下从黑色透明度0逐渐变为黑色透明度1
val alpha = smoothProgress.coerceIn(0f, 1f)
Color.Black.copy(alpha = alpha)
} else {
// 亮色模式:初始状态:半透明深色,让白色图标清晰可见
val initialDarkAlpha = 0.12f
// 使用平滑的插值函数,让整个过渡更自然
val easedProgress = smoothProgress * smoothProgress * (3f - 2f * smoothProgress) // smoothstep
// 使用平滑的插值函数,让整个过渡更自然
val easedProgress = smoothProgress * smoothProgress * (3f - 2f * smoothProgress) // smoothstep
// 颜色值:从黑色(0)平滑过渡到白色(1)
val colorValue = easedProgress
// 颜色值:从黑色(0)平滑过渡到白色(1)
val colorValue = easedProgress
// 透明度:从初始值(0.12f)逐渐减少到0
// 当smoothProgress从0到1时alpha从initialDarkAlpha减少到0
val alpha = initialDarkAlpha * (1f - easedProgress)
// 透明度:从初始值(0.12f)逐渐减少到0
// 当smoothProgress从0到1时alpha从initialDarkAlpha减少到0
val alpha = initialDarkAlpha * (1f - easedProgress)
Color(
red = colorValue,
green = colorValue,
blue = colorValue,
alpha = alpha.coerceIn(0f, initialDarkAlpha)
)
Color(
red = colorValue,
green = colorValue,
blue = colorValue,
alpha = alpha.coerceIn(0f, initialDarkAlpha)
)
}
}
Box(
@@ -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,25 +780,19 @@ fun TopNavigationBar(
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
)
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 ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color.White
colorFilter = ColorFilter.tint(backButtonColor) // 深色模式下为白色,亮色模式下为黑色
)
// 未设置背景的用户(自己的主页且没有背景图)显示昵称
if (isSelf && !hasBanner && profile != null) {
Spacer(modifier = Modifier.width(8.dp))
Text(
text = profile.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = backButtonColor // 深色模式下为白色,亮色模式下为黑色
)
}
}
}
}

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
@@ -199,19 +201,27 @@ fun GalleryGrid(
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = "你的故事还没开始",
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))
Text(
text = "发布一条动态,和世界打个招呼吧",
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
@@ -27,27 +26,25 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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))
// 分段控制器
@@ -71,10 +68,14 @@ fun GroupChatEmptyContent() {
// 空状态文本
Text(
text = "空空如也~",
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
)
}
}
@@ -86,6 +87,7 @@ private fun SegmentedControl(
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
@@ -94,30 +96,33 @@ private fun SegmentedControl(
) {
// 全部
SegmentButton(
text = "全部",
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))
// 公开
SegmentButton(
text = "公开",
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))
// 私有
SegmentButton(
text = "私有",
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp
width = 54.dp,
appColors = AppColors
)
}
}
@@ -127,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
@@ -135,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)
},
@@ -148,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))
// 分段控制器
@@ -247,19 +240,27 @@ fun AgentEmptyContentWithSegments() {
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = "专属AI等你召唤",
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))
Text(
text = "AI将成为你的伙伴而不是工具",
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),
@@ -311,30 +313,33 @@ private fun AgentSegmentedControl(
) {
// 全部
AgentSegmentButton(
text = "全部",
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))
// 公开
AgentSegmentButton(
text = "公开",
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))
// 私有
AgentSegmentButton(
text = "私有",
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

@@ -10,21 +10,14 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -52,7 +45,7 @@ fun UserContentPageIndicator(
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 图片/相册 Tab
// 动态 Tab
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
@@ -64,15 +57,24 @@ fun UserContentPageIndicator(
}
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_images),
contentDescription = "Gallery",
tint = if (pagerState.currentPage == 0) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
Text(
text = stringResource(R.string.index_dynamic),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 0) FontWeight.SemiBold else FontWeight.Medium,
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) {
Box(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(AppColors.text)
)
}
}
// Agent Tab (只在非智能体用户时显示)
// 智能体 Tab (只在非智能体用户时显示)
if (showAgentTab) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -85,39 +87,53 @@ fun UserContentPageIndicator(
}
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_ai),
contentDescription = "Agents",
tint = if (pagerState.currentPage == 1) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
Text(
text = stringResource(R.string.chat_ai),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 1) FontWeight.SemiBold else FontWeight.Medium,
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) {
Box(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(AppColors.text)
)
}
}
}
}
// 下划线指示器
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.height(2.dp)
.background(
if (pagerState.currentPage == 0) AppColors.text else Color.Transparent
)
)
// 群聊 Tab (只在非智能体用户时显示)
if (showAgentTab) {
Box(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.height(2.dp)
.background(
if (pagerState.currentPage == 1) AppColors.text else Color.Transparent
.noRippleClickable {
scope.launch {
pagerState.scrollToPage(2)
}
}
.padding(vertical = 12.dp)
) {
Text(
text = stringResource(R.string.chat_group),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 2) FontWeight.SemiBold else FontWeight.Medium,
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) {
Box(
modifier = Modifier
.width(20.dp)
.height(2.dp)
.background(AppColors.text)
)
)
}
}
}
}
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
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
@@ -117,15 +118,15 @@ 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))
Text(
text = "帖子",
text = stringResource(R.string.posts),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -152,15 +153,15 @@ 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))
Text(
text = "粉丝",
text = stringResource(R.string.followers_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -188,15 +189,15 @@ 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))
Text(
text = "关注",
text = stringResource(R.string.following_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
color = AppColors.text,
textAlign = TextAlign.Center
)
}
@@ -216,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))
@@ -226,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
)
@@ -234,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
)
@@ -257,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
)
}
@@ -266,19 +267,19 @@ fun UserItem(
ProfileTag(
text = zodiac,
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
textColor = Color(0xFF000000)
textColor = AppColors.text
)
}
// 编辑标签(仅自己可见)
if (isSelf) {
ProfileTag(
text = "编辑",
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)
)
},
@@ -333,7 +334,7 @@ private fun EditIcon(
) {
Image(
painter = painterResource(id = R.mipmap.bi),
contentDescription = "编辑",
contentDescription = stringResource(R.string.edit_profile),
modifier = modifier,
colorFilter = ColorFilter.tint(color)
)

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()
Box(modifier = Modifier.align(Alignment.BottomStart)) {
VideoBottom.invoke()
if (moment != null && VideoBottom != null) {
Box(modifier = Modifier.align(Alignment.BottomStart)) {
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,70 +370,107 @@ fun VideoPlayer(
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
UserAvatar()
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k")
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") {
showCommentModal = true
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 = "0")
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "0") {
showCommentModal = true
}
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "0")
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "0")
}
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "234")
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "677k")
}
}
// info
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomStart
) {
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
Row(
modifier = Modifier
.padding(bottom = 8.dp)
.background(color = Color.Gray),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Image(
modifier = Modifier
.size(20.dp)
.padding(start = 4.dp, end = 6.dp),
painter = painterResource(id = R.drawable.rider_pro_video_location),
contentDescription = ""
)
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)
.background(color = Color.Gray),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
Image(
modifier = Modifier
.size(20.dp)
.padding(start = 4.dp, end = 6.dp),
painter = painterResource(id = R.drawable.rider_pro_video_location),
contentDescription = ""
)
Text(
modifier = Modifier.padding(end = 4.dp),
text = moment.location,
fontSize = 12.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
}
Text(
modifier = Modifier.padding(end = 4.dp),
text = "USA",
fontSize = 12.sp,
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 = moment.momentTextContent,
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
}
}
Text(
text = "@Kevinlinpr",
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp), // 确保Text占用可用宽度
text = "Pedro Acosta to join KTM in 2025 on a multi-year deal! \uD83D\uDFE0",
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),
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

@@ -12,6 +12,7 @@
<string name="users">ユーザー</string>
<string name="like_upper">いいね</string>
<string name="followers_upper">フォロワー</string>
<string name="posts">投稿</string>
<string name="favourites_upper">お気に入り</string>
<string name="favourites_null">あれ、何もない。..</string>
<string name="notifications_upper">通知</string>
@@ -52,6 +53,10 @@
<string name="error_not_accept_term">最高のサービスを提供するために、登録前に利用規約を読み、同意してください。</string>
<string name="empty_my_post_title">まだ投稿がありません</string>
<string name="empty_my_post_content">今すぐモーメントを投稿</string>
<string name="story_not_started">ストーリーはまだ始まっていません</string>
<string name="your_story_not_started">あなたのストーリーはまだ始まっていません</string>
<string name="publish_moment_greeting">モーメントを投稿して、世界に挨拶しましょう</string>
<string name="no_image">画像がありません</string>
<string name="edit_profile">プロフィールを編集</string>
<string name="share">シェア</string>
<string name="logout">ログアウト</string>
@@ -144,17 +149,21 @@
<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>
<string name="chat_group">グループ</string>
<string name="chat_friend">友達</string>
<string name="chat_all">すべて</string>
<string name="public_label">公開</string>
<string name="private_label">プライベート</string>
<string name="chatting_now">人はおしゃべりをしている…</string>
<string name="agent_chat_list_title">AIエージェントチャット</string>
<string name="agent_chat_empty_title">AIエージェントチャットがありません</string>
<string name="agent_chat_empty_subtitle">AIエージェントと対話してみましょう</string>
<string name="exclusive_ai_waiting">専属AIがあなたを待っています</string>
<string name="ai_companion_not_tool">AIはあなたのパートナーとなり、ツールではありません</string>
<string name="agent_chat_me_prefix">私: </string>
<string name="agent_chat_image">[画像]</string>
<string name="agent_chat_voice">[音声]</string>
@@ -166,10 +175,15 @@
<string name="agent_chat_user_info_failed">ユーザー情報の取得に失敗しました: %s</string>
<string name="group_chat_empty">グループチャットがありません</string>
<string name="group_chat_empty_join">まだどのグループチャットにも参加していません</string>
<string name="empty_nothing">何もありません~</string>
<string name="group_chat_empty_title">グループチャットメッセージのない宇宙は静かすぎます</string>
<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>
@@ -255,9 +269,28 @@
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTIタイプ</string>
<string name="zodiac">星座</string>
<string name="change_cover">カバーを変更</string>
<string name="personal_intro">自己紹介</string>
<string name="error_nickname_empty">ニックネームは空にできません</string>
<string name="error_nickname_too_short">ニックネームの長さは3文字以上である必要があります</string>
<string name="error_nickname_too_long">ニックネームの長さは20文字以下である必要があります</string>
<string name="error_bio_too_long">自己紹介の長さは100文字以下である必要があります</string>
<string name="error_load_profile_failed">ユーザープロフィールの読み込みに失敗しました。もう一度お試しください</string>
<string name="save">保存</string>
<string name="choose_mbti">MBTIを選択</string>
<string name="choose_zodiac">星座を選択</string>
<string name="zodiac_aries">牡羊座</string>
<string name="zodiac_taurus">牡牛座</string>
<string name="zodiac_gemini">双子座</string>
<string name="zodiac_cancer">蟹座</string>
<string name="zodiac_leo">獅子座</string>
<string name="zodiac_virgo">乙女座</string>
<string name="zodiac_libra">天秤座</string>
<string name="zodiac_scorpio">蠍座</string>
<string name="zodiac_sagittarius">射手座</string>
<string name="zodiac_capricorn">山羊座</string>
<string name="zodiac_aquarius">水瓶座</string>
<string name="zodiac_pisces">魚座</string>
<!-- Side Menu -->
<string name="scan_qr">さっと動かす</string>

View File

@@ -12,6 +12,7 @@
<string name="users">用户</string>
<string name="like_upper"></string>
<string name="followers_upper">粉丝</string>
<string name="posts">帖子</string>
<string name="favourites_upper">收藏</string>
<string name="notifications_upper">消息</string>
<string name="following_upper">关注</string>
@@ -51,6 +52,10 @@
<string name="error_not_accept_term">"为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 "</string>
<string name="empty_my_post_title">还没有发布任何动态</string>
<string name="empty_my_post_content">发布一个动态吧</string>
<string name="story_not_started">故事还没开始</string>
<string name="your_story_not_started">你的故事还没开始</string>
<string name="publish_moment_greeting">发布一条动态,和世界打个招呼吧</string>
<string name="no_image">暂无图片</string>
<string name="edit_profile">编辑</string>
<string name="share">分享</string>
<string name="logout">登出</string>
@@ -153,11 +158,15 @@
<string name="chat_group">群聊</string>
<string name="chat_friend">朋友</string>
<string name="chat_all">全部</string>
<string name="public_label">公开</string>
<string name="private_label">私有</string>
<string name="favourites_null">咦,什么都没有...</string>
<string name="agent_chat_list_title">智能体聊天</string>
<string name="agent_chat_empty_title">AI 在等你的开场白</string>
<string name="agent_chat_empty_subtitle">去首页探索一下,主动发起对话!</string>
<string name="exclusive_ai_waiting">专属AI等你召唤</string>
<string name="ai_companion_not_tool">AI将成为你的伙伴而不是工具</string>
<string name="agent_chat_me_prefix">我: </string>
<string name="agent_chat_image">[图片]</string>
<string name="agent_chat_voice">[语音]</string>
@@ -169,10 +178,15 @@
<string name="agent_chat_user_info_failed">获取用户信息失败: %s</string>
<string name="group_chat_empty">没有群聊,宇宙好安静</string>
<string name="group_chat_empty_title">没有群聊消息的宇宙太安静了</string>
<string name="empty_nothing">空空如也~</string>
<string name="group_chat_empty_subtitle">在首页探索感兴趣的主题房间</string>
<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>
@@ -292,9 +306,28 @@
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI 类型</string>
<string name="zodiac">星座</string>
<string name="change_cover">更换封面</string>
<string name="personal_intro">个人简介</string>
<string name="error_nickname_empty">昵称不能为空</string>
<string name="error_nickname_too_short">昵称长度不能小于3</string>
<string name="error_nickname_too_long">昵称长度不能大于20</string>
<string name="error_bio_too_long">个人简介长度不能大于100</string>
<string name="error_load_profile_failed">加载用户资料失败,请重试</string>
<string name="save">保存</string>
<string name="choose_mbti">选择 MBTI</string>
<string name="choose_zodiac">选择星座</string>
<string name="zodiac_aries">白羊座</string>
<string name="zodiac_taurus">金牛座</string>
<string name="zodiac_gemini">双子座</string>
<string name="zodiac_cancer">巨蟹座</string>
<string name="zodiac_leo">狮子座</string>
<string name="zodiac_virgo">处女座</string>
<string name="zodiac_libra">天秤座</string>
<string name="zodiac_scorpio">天蝎座</string>
<string name="zodiac_sagittarius">射手座</string>
<string name="zodiac_capricorn">摩羯座</string>
<string name="zodiac_aquarius">水瓶座</string>
<string name="zodiac_pisces">双鱼座</string>
<!-- Side Menu -->
<string name="scan_qr">扫一扫</string>

View File

@@ -11,6 +11,7 @@
<string name="users">Users</string>
<string name="like_upper">LIKE</string>
<string name="followers_upper">FOLLOWERS</string>
<string name="posts">Posts</string>
<string name="favourites_upper">FAVOURITES</string>
<string name="favourites_null">Well,nothing </string>
<string name="notifications_upper">NOTIFICATIONS</string>
@@ -51,6 +52,10 @@
<string name="error_not_accept_term">To provide you with the best service, please read and agree to our User Agreement before registering.</string>
<string name="empty_my_post_title">You haven\'t left any tracks yet</string>
<string name="empty_my_post_content">Post a moment now</string>
<string name="story_not_started">Your story hasn\'t started yet</string>
<string name="your_story_not_started">Your story hasn\'t started yet</string>
<string name="publish_moment_greeting">Post a moment and say hello to the world</string>
<string name="no_image">No image</string>
<string name="edit_profile">Edit profile</string>
<string name="share">share</string>
<string name="logout">Logout</string>
@@ -143,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>
@@ -151,9 +156,13 @@
<string name="chat_friend">Friends</string>
<string name="chatting_now">people chatting now…</string>
<string name="chat_all">All</string>
<string name="public_label">Public</string>
<string name="private_label">Private</string>
<string name="agent_chat_list_title">Agent Chat</string>
<string name="agent_chat_empty_title">No Agent Chat</string>
<string name="agent_chat_empty_subtitle">Start chatting with agents</string>
<string name="exclusive_ai_waiting">Exclusive AI waiting for you</string>
<string name="ai_companion_not_tool">AI will be your companion, not a tool</string>
<string name="agent_chat_me_prefix">Me: </string>
<string name="agent_chat_image">[Image]</string>
<string name="agent_chat_voice">[Voice]</string>
@@ -165,10 +174,15 @@
<string name="agent_chat_user_info_failed">Failed to get user info: %s</string>
<string name="group_chat_empty">No group chats</string>
<string name="group_chat_empty_join">You have not joined any group chats yet</string>
<string name="empty_nothing">Nothing here~</string>
<string name="group_chat_empty_title">The universe is too quiet without group chat messages</string>
<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>
@@ -286,9 +300,28 @@
<!-- Edit Profile Extras -->
<string name="mbti_type">MBTI</string>
<string name="zodiac">Zodiac</string>
<string name="change_cover">Change Cover</string>
<string name="personal_intro">Personal Introduction</string>
<string name="error_nickname_empty">Nickname cannot be empty</string>
<string name="error_nickname_too_short">Nickname length cannot be less than 3</string>
<string name="error_nickname_too_long">Nickname length cannot be greater than 20</string>
<string name="error_bio_too_long">Bio length cannot be greater than 100</string>
<string name="error_load_profile_failed">Failed to load user profile, please try again</string>
<string name="save">Save</string>
<string name="choose_mbti">Choose MBTI</string>
<string name="choose_zodiac">Choose Zodiac</string>
<string name="zodiac_aries">Aries</string>
<string name="zodiac_taurus">Taurus</string>
<string name="zodiac_gemini">Gemini</string>
<string name="zodiac_cancer">Cancer</string>
<string name="zodiac_leo">Leo</string>
<string name="zodiac_virgo">Virgo</string>
<string name="zodiac_libra">Libra</string>
<string name="zodiac_scorpio">Scorpio</string>
<string name="zodiac_sagittarius">Sagittarius</string>
<string name="zodiac_capricorn">Capricorn</string>
<string name="zodiac_aquarius">Aquarius</string>
<string name="zodiac_pisces">Pisces</string>
<!-- Side Menu -->
<string name="scan_qr">Scan QR</string>