27 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
6590b09300 Merge pull request #55 from Kevinlinpr/zhong_1
修改BUG:登录账号邮箱格式错误无提示
2025-11-10 14:06:17 +08:00
30563a75f3 Merge branch 'main' into zhong_1 2025-11-10 11:20:08 +08:00
aac3220a69 Merge pull request #56 from Kevinlinpr/feat/pr-20251104-154907
我的(侧边栏)ui调整
2025-11-10 11:05:12 +08:00
e2de134180 Merge branch 'main' into zhong_1
Merge main branch to keep zhong_1 up to date# Please enter a commit message to explain why this merge is necessary,
2025-11-10 10:19:44 +08:00
7f1be94896 Merge pull request #54 from Kevinlinpr/atm2
优化分类数据加载的语言参数
2025-11-10 10:07:35 +08:00
1c048fd9c0 发布动态页面UI调整 2025-11-07 21:30:56 +08:00
2613d2e801 修复bug:关注用户或者AI后关注列表能正常显示 2025-11-07 21:29:03 +08:00
c100a8ceef 注册账号界面ui调整 2025-11-07 21:25:02 +08:00
f86b5e1d39 账号安全界面ui设置 2025-11-07 21:22:10 +08:00
75eb38b188 我的界面ui设置,新加群聊标签以及缺省图 2025-11-07 21:19:17 +08:00
4f588483c0 Merge branch 'feat/pr-20251104-154907' of https://github.com/Kevinlinpr/rider-pro-android-app into feat/pr-20251104-154907 2025-11-07 14:42:17 +08:00
0bc442762d 我的-页面顶部导航栏ui修改,增加下滑时顶部导航栏的变化效果以及壁纸头像大小修正 2025-11-07 14:36:25 +08:00
397ac6a9ee Merge branch 'main' into feat/pr-20251104-154907 2025-11-07 13:56:53 +08:00
784f87dc39 recover 2025-11-07 10:46:08 +08:00
e714f567b9 我的-编辑-ui调整 2025-11-06 21:34:08 +08:00
703beb8d43 我的(侧边栏)ui调整 2025-11-06 21:23:51 +08:00
2b30beb367 资源文件替换 2025-11-06 20:56:20 +08:00
6fffa0447e 修复文件将所有 PromptRule 引用改为 AgentRule,并更新相关字段访问 2025-11-06 20:49:10 +08:00
2a9d6a2f6b 抓包配置文件 2025-11-06 18:15:00 +08:00
cc12a08472 修改BUG:登录账号邮箱格式错误无提示 2025-11-06 10:53:34 +08:00
513897499d 优化分类数据加载的语言参数
根据系统语言标签(如 "zh-CN")将其转换为后端支持的语言代码(zh, cn, ja),用于请求分类数据。默认回退到 "zh"。
2025-11-06 10:20:28 +08:00
baa6f284bd Merge pull request #53 from Kevinlinpr/atm
新增智能体和房间规则管理功能
2025-11-06 10:09:16 +08:00
171 changed files with 3469 additions and 1090 deletions

View File

@@ -19,6 +19,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.RaveNow" android:theme="@style/Theme.RaveNow"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31"> tools:targetApi="31">
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"

View File

@@ -4,6 +4,7 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentImageEntity import com.aiosman.ravenow.entity.MomentImageEntity
import com.aiosman.ravenow.entity.MomentVideoEntity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.io.File import java.io.File
@@ -12,8 +13,12 @@ data class Moment(
val id: Long, val id: Long,
@SerializedName("textContent") @SerializedName("textContent")
val textContent: String, val textContent: String,
@SerializedName("url")
val url: String? = null,
@SerializedName("images") @SerializedName("images")
val images: List<Image>, val images: List<Image>? = null,
@SerializedName("videos")
val videos: List<Video>? = null,
@SerializedName("user") @SerializedName("user")
val user: User, val user: User,
@SerializedName("likeCount") @SerializedName("likeCount")
@@ -24,7 +29,7 @@ data class Moment(
val favoriteCount: Long, val favoriteCount: Long,
@SerializedName("isFavorite") @SerializedName("isFavorite")
val isFavorite: Boolean, val isFavorite: Boolean,
@SerializedName("shareCount") @SerializedName("isCommented")
val isCommented: Boolean, val isCommented: Boolean,
@SerializedName("commentCount") @SerializedName("commentCount")
val commentCount: Long, val commentCount: Long,
@@ -47,6 +52,14 @@ data class Moment(
val newsLanguage: String? = null, val newsLanguage: String? = null,
@SerializedName("newsContent") @SerializedName("newsContent")
val newsContent: String? = null, 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 { fun toMomentItem(): MomentEntity {
return MomentEntity( return MomentEntity(
@@ -62,7 +75,7 @@ data class Moment(
commentCount = commentCount.toInt(), commentCount = commentCount.toInt(),
shareCount = 0, shareCount = 0,
favoriteCount = favoriteCount.toInt(), favoriteCount = favoriteCount.toInt(),
images = images.map { images = images?.map {
MomentImageEntity( MomentImageEntity(
url = "${ApiClient.BASE_SERVER}${it.url}", url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}", thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
@@ -71,10 +84,28 @@ data class Moment(
width = it.width, width = it.width,
height = it.height height = it.height
) )
}, } ?: emptyList(),
authorId = user.id.toInt(), authorId = user.id.toInt(),
liked = isLiked, liked = isLiked,
isFavorite = isFavorite, 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, isNews = isNews,
newsTitle = newsTitle ?: "", newsTitle = newsTitle ?: "",
@@ -82,7 +113,11 @@ data class Moment(
newsSource = newsSource ?: "", newsSource = newsSource ?: "",
newsCategory = newsCategory ?: "", newsCategory = newsCategory ?: "",
newsLanguage = newsLanguage ?: "", newsLanguage = newsLanguage ?: "",
newsContent = newsContent ?: "" newsContent = newsContent ?: "",
hasFullText = hasFullText,
summary = summary,
publishedAt = publishedAt,
imageCached = imageCached
) )
} }
} }
@@ -92,8 +127,26 @@ data class Image(
val id: Long, val id: Long,
@SerializedName("url") @SerializedName("url")
val url: String, val url: String,
@SerializedName("original_url")
val originalUrl: String? = null,
@SerializedName("directUrl")
val directUrl: String? = null,
@SerializedName("thumbnail") @SerializedName("thumbnail")
val thumbnail: String, 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") @SerializedName("blurHash")
val blurHash: String?, val blurHash: String?,
@SerializedName("width") @SerializedName("width")
@@ -102,13 +155,68 @@ data class Image(
val height: Int? 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( data class User(
@SerializedName("id") @SerializedName("id")
val id: Long, val id: Long,
@SerializedName("nickName") @SerializedName("nickName")
val nickName: String, val nickName: String,
@SerializedName("avatar") @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( data class UploadImage(

View File

@@ -97,7 +97,8 @@ class UserServiceImpl : UserService {
pageSize = pageSize, pageSize = pageSize,
search = nickname, search = nickname,
followerId = followerId, followerId = followerId,
followingId = followingId followingId = followingId,
includeAI = true
) )
val body = resp.body() ?: throw ServiceException("Failed to get account") val body = resp.body() ?: throw ServiceException("Failed to get account")
return ListContainer<AccountProfileEntity>( return ListContainer<AccountProfileEntity>(

View File

@@ -800,6 +800,7 @@ interface RaveNowAPI {
@Query("favouriteUserId") favouriteUserId: Int? = null, @Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null, @Query("explore") explore: String? = null,
@Query("newsFilter") newsFilter: String? = null, @Query("newsFilter") newsFilter: String? = null,
@Query("videoFilter") videoFilter: String? = null,
): Response<ListContainer<Moment>> ): Response<ListContainer<Moment>>
@Multipart @Multipart
@@ -954,7 +955,7 @@ interface RaveNowAPI {
@Query("nickname") search: String? = null, @Query("nickname") search: String? = null,
@Query("followerId") followerId: Int? = null, @Query("followerId") followerId: Int? = null,
@Query("followingId") followingId: Int? = null, @Query("followingId") followingId: Int? = null,
@Query("includeAI") includeAI: Boolean? = false, @Query("includeAI") includeAI: Boolean? = true,
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true, @Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
): Response<ListContainer<AccountProfile>> ): Response<ListContainer<AccountProfile>>

View File

@@ -260,6 +260,38 @@ data class MomentImageEntity(
var height: Int? = null 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 relMoment: MomentEntity? = null,
// 是否收藏 // 是否收藏
var isFavorite: Boolean = false, var isFavorite: Boolean = false,
// 外部链接
val url: String? = null,
// 动态视频列表
val videos: List<MomentVideoEntity>? = null,
// 新闻相关字段 // 新闻相关字段
val isNews: Boolean = false, val isNews: Boolean = false,
val newsTitle: String = "", val newsTitle: String = "",
@@ -307,13 +343,22 @@ data class MomentEntity(
val newsSource: String = "", val newsSource: String = "",
val newsCategory: String = "", val newsCategory: String = "",
val newsLanguage: 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( class MomentLoaderExtraArgs(
val explore: Boolean? = false, val explore: Boolean? = false,
val timelineId: Int? = null, val timelineId: Int? = null,
val authorId : Int? = null, val authorId : Int? = null,
val newsOnly: Boolean? = null val newsOnly: Boolean? = null,
val videoOnly: Boolean? = null
) )
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() { class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData( override suspend fun fetchData(
@@ -327,7 +372,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
explore = if (extra.explore == true) "true" else "", explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId, timelineId = extra.timelineId,
authorId = extra.authorId, 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 { val data = result.body()?.let {
ListContainer( ListContainer(

View File

@@ -21,6 +21,8 @@ object AccountEditViewModel : ViewModel() {
var name by mutableStateOf("") var name by mutableStateOf("")
var bio by mutableStateOf("") var bio by mutableStateOf("")
var imageUrl by mutableStateOf<Uri?>(null) var imageUrl by mutableStateOf<Uri?>(null)
var bannerImageUrl by mutableStateOf<Uri?>(null)
var bannerFile by mutableStateOf<File?>(null)
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
var croppedBitmap by mutableStateOf<Bitmap?>(null) var croppedBitmap by mutableStateOf<Bitmap?>(null)
@@ -82,6 +84,30 @@ object AccountEditViewModel : ViewModel() {
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream()) it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
UploadImage(file, "avatar.jpg", "", "jpg") 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 cleanName = name.trim().replace("\n", "").replace("\r", "")
val cleanBio = bio.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 val newName = if (cleanName == profile?.nickName) null else cleanName
accountService.updateProfile( accountService.updateProfile(
avatar = newAvatar, avatar = newAvatar,
banner = null, banner = newBanner,
nickName = newName, nickName = newName,
bio = cleanBio bio = cleanBio
) )
@@ -100,6 +126,9 @@ object AccountEditViewModel : ViewModel() {
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac) com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
} }
} catch (_: Exception) { } } catch (_: Exception) { }
// 清除背景图状态
bannerImageUrl = null
bannerFile = null
// 刷新用户资料 // 刷新用户资料
reloadProfile() reloadProfile()
// 刷新个人资料页面的用户资料 // 刷新个人资料页面的用户资料
@@ -116,6 +145,8 @@ object AccountEditViewModel : ViewModel() {
name = "" name = ""
bio = "" bio = ""
imageUrl = null imageUrl = null
bannerImageUrl = null
bannerFile = null
croppedBitmap = null croppedBitmap = null
isUpdating = false isUpdating = false
isLoading = false isLoading = false

View File

@@ -1,117 +1,177 @@
package com.aiosman.ravenow.ui.account package com.aiosman.ravenow.ui.account
import android.widget.Toast import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.Messaging
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.index.NavItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
private object AccountSettingConstants {
const val BACK_BUTTON_SIZE = 36
const val BACK_BUTTON_ICON_SIZE = 24
const val BACK_BUTTON_START_PADDING = 19
const val OPTION_ITEM_HEIGHT = 56
const val OPTION_ITEM_ICON_SIZE = 24
const val OPTION_ITEM_HORIZONTAL_PADDING = 16
const val OPTION_ITEM_ICON_TEXT_SPACING = 12
const val OPTION_ITEM_TEXT_SIZE = 17
const val HEADER_VERTICAL_PADDING = 16
const val TITLE_OFFSET_X = 19
const val CARD_CORNER_RADIUS = 16
}
@Composable
private fun CircularBackButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val appColors = LocalAppTheme.current
Box(
modifier = modifier
.size(AccountSettingConstants.BACK_BUTTON_SIZE.dp)
.background(
color = appColors.secondaryBackground,
shape = CircleShape
)
.noRippleClickable { onClick() },
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier.size(AccountSettingConstants.BACK_BUTTON_ICON_SIZE.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
}
}
@Composable
private fun SecurityOptionItem(
iconRes: Int,
label: String,
onClick: () -> Unit,
applyColorFilter: Boolean = true
) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
.height(AccountSettingConstants.OPTION_ITEM_HEIGHT.dp)
.padding(horizontal = AccountSettingConstants.OPTION_ITEM_HORIZONTAL_PADDING.dp)
.noRippleClickable { onClick() },
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = Modifier.size(AccountSettingConstants.OPTION_ITEM_ICON_SIZE.dp),
colorFilter = if (applyColorFilter) ColorFilter.tint(appColors.text) else null
)
Text(
text = label,
modifier = Modifier
.padding(start = AccountSettingConstants.OPTION_ITEM_ICON_TEXT_SPACING.dp)
.weight(1f),
color = appColors.text,
fontSize = AccountSettingConstants.OPTION_ITEM_TEXT_SIZE.sp,
fontWeight = FontWeight.Medium
)
Image(
painter = painterResource(id = R.drawable.rave_now_nav_right),
contentDescription = null,
modifier = Modifier.size(AccountSettingConstants.OPTION_ITEM_ICON_SIZE.dp),
colorFilter = ColorFilter.tint(appColors.secondaryText)
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccountSetting() { fun AccountSetting() {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(appColors.background), .background(appColors.background),
) { ) {
StatusBarSpacer() StatusBarSpacer()
// 顶部标题栏
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(vertical = AccountSettingConstants.HEADER_VERTICAL_PADDING.dp)
) { ) {
NoticeScreenHeader( CircularBackButton(
title = stringResource(R.string.account_and_security), onClick = { navController.navigateUp() },
moreIcon = false modifier = Modifier.padding(start = AccountSettingConstants.BACK_BUTTON_START_PADDING.dp)
)
Text(
text = stringResource(R.string.account_and_security),
fontWeight = FontWeight.W800,
fontSize = AccountSettingConstants.OPTION_ITEM_TEXT_SIZE.sp,
color = appColors.text,
modifier = Modifier
.align(Alignment.Center)
.offset(x = AccountSettingConstants.TITLE_OFFSET_X.dp)
) )
} }
// 安全选项卡片
Column( Column(
modifier = Modifier.padding(start = 24.dp) modifier = Modifier
.fillMaxWidth()
.background(
color = appColors.background,
shape = RoundedCornerShape(AccountSettingConstants.CARD_CORNER_RADIUS.dp)
)
) { ) {
Box( SecurityOptionItem(
modifier = Modifier iconRes = R.mipmap.icons_padlock,
.fillMaxWidth() label = stringResource(R.string.change_password),
.padding(vertical = 8.dp) onClick = { navController.navigate(NavigationRoute.ChangePasswordScreen.route) }
) {
NavItem(
iconRes = R.mipmap.rider_pro_change_password,
label = stringResource(R.string.change_password),
modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
)
}
// 分割线
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(appColors.divider)
) )
Box(
modifier = Modifier SecurityOptionItem(
.fillMaxWidth() iconRes = R.mipmap.icons_block,
.padding(vertical = 8.dp) label = stringResource(R.string.blocked_users),
) { onClick = {
NavItem( // TODO: 导航到屏蔽用户页面
iconRes = R.drawable.rider_pro_moment_delete, }
label = stringResource(R.string.remove_account), )
modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.RemoveAccountScreen.route) SecurityOptionItem(
} iconRes = R.mipmap.icons_remove,
) label = stringResource(R.string.remove_account),
} onClick = { navController.navigate(NavigationRoute.RemoveAccountScreen.route) },
Box( applyColorFilter = false
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(appColors.divider)
) )
} }
} }
} }

View File

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

View File

@@ -29,24 +29,55 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// 星座列表 // 星座资源ID列表
val ZODIAC_SIGNS = listOf( 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 @Composable
fun ZodiacSelectScreen() { fun ZodiacSelectScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val model = AccountEditViewModel val model = AccountEditViewModel
val currentZodiac = model.zodiac val currentZodiacResId = findZodiacResId(model.zodiac)
Column( Column(
modifier = Modifier modifier = Modifier
@@ -70,12 +101,20 @@ fun ZodiacSelectScreen() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp) 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( ZodiacItem(
zodiac = zodiac, zodiac = zodiacText,
isSelected = zodiac == currentZodiac, zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId,
onClick = { onClick = {
model.zodiac = zodiac // 保存当前语言的星座文本
model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
navController.navigateUp() navController.navigateUp()
} }
) )
@@ -88,6 +127,7 @@ fun ZodiacSelectScreen() {
@Composable @Composable
fun ZodiacItem( fun ZodiacItem(
zodiac: String, zodiac: String,
zodiacResId: Int,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {

View File

@@ -4,19 +4,26 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit as EditIcon
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -28,10 +35,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
@@ -39,28 +50,29 @@ import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.form.FormTextInput
import com.aiosman.ravenow.ui.composables.debouncedClickable import com.aiosman.ravenow.ui.composables.debouncedClickable
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import android.widget.Toast
import java.io.File import java.io.File
/** /**
@@ -86,6 +98,10 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
quality = 100 quality = 100
) { uri, file -> ) { uri, file ->
// 处理选中的图片 // 处理选中的图片
// 保存到 ViewModel 中,等待保存时一起上传
model.bannerImageUrl = uri
model.bannerFile = file
// 如果提供了回调,也调用它(用于个人主页直接更新)
onUpdateBanner?.invoke(uri, file, context) onUpdateBanner?.invoke(uri, file, context)
} }
@@ -93,10 +109,21 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 去除换行符,确保昵称不包含换行 // 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "") val cleanValue = value.replace("\n", "").replace("\r", "")
model.name = cleanValue model.name = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
usernameError = when { usernameError = when {
cleanValue.trim().isEmpty() -> "昵称不能为空" cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
cleanValue.length < 3 -> "昵称长度不能小于3" cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
cleanValue.length > 20 -> "昵称长度不能大于20" 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 else -> null
} }
} }
@@ -107,8 +134,17 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 去除换行符,确保个人简介不包含换行 // 去除换行符,确保个人简介不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "") val cleanValue = value.replace("\n", "").replace("\r", "")
model.bio = cleanValue model.bio = cleanValue
// 实时验证,但不显示错误(只在保存时显示)
bioError = when { 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 else -> null
} }
} }
@@ -134,192 +170,28 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据 // 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
model.reloadProfile() model.reloadProfile()
} }
// 设置状态栏为透明,根据暗色模式决定图标颜色
val systemUiController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
}
StatusBarMaskLayout( StatusBarMaskLayout(
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp), modifier = Modifier.background(appColors.background),
darkIcons = !AppState.darkMode, darkIcons = !AppState.darkMode, // 根据暗色模式决定图标颜色
maskBoxBackgroundColor = appColors.background maskBoxBackgroundColor = Color.Transparent
) { ) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = appColors.background), .background(appColors.background)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
//StatusBarSpacer()
Box(
modifier = Modifier.padding(horizontal = 0.dp, vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.edit_profile),
moreIcon = false
) {
Icon(
modifier = Modifier
.size(24.dp)
.debouncedClickable(
enabled = validate() && !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
}
}
}
},
imageVector = Icons.Default.Check,
contentDescription = "保存",
tint = if (validate() && !model.isUpdating) appColors.text else appColors.nonActiveText
)
}
}
// 添加横幅图片区域
val banner = model.profile?.banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.clip(RoundedCornerShape(12.dp))
) {
if (banner != null) {
CustomAsyncImage(
context = LocalContext.current,
imageUrl = banner,
modifier = Modifier.fillMaxSize(),
contentDescription = "Banner",
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.1f))
)
}
Box(
modifier = Modifier
.width(120.dp)
.height(42.dp)
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(9.dp)
)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
){
Text(
text = "change",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 显示内容或加载状态
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
when { when {
model.profile != null -> {
Log.d("AccountEditScreen2", "显示用户资料内容")
// 有数据时显示内容
val it = model.profile!!
Box(
modifier = Modifier.size(88.dp),
contentAlignment = Alignment.Center
) {
CustomAsyncImage(
context,
model.croppedBitmap ?: it.avatar,
modifier = Modifier
.size(88.dp)
.clip(
RoundedCornerShape(88.dp)
),
contentDescription = "",
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x997c68ef),
Color(0xFF7bd8f8)
)
),
)
.align(Alignment.BottomEnd)
.debouncedClickable(
debounceTime = 800L
) {
debouncedNavigation {
navController.navigate(NavigationRoute.ImageCrop.route)
}
},
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Add,
contentDescription = "Add",
tint = Color.White,
)
}
}
Spacer(modifier = Modifier.height(18.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
) {
FormTextInput(
value = model.name,
label = stringResource(R.string.nickname),
hint = "Input nickname",
modifier = Modifier.fillMaxWidth(),
error = usernameError
) { value ->
onNicknameChange(value)
}
FormTextInput(
value = model.bio,
label = stringResource(R.string.bio),
hint = "Input bio",
modifier = Modifier.fillMaxWidth(),
error = bioError
) { value ->
onBioChange(value)
}
}
}
model.isLoading -> { model.isLoading -> {
Log.d("AccountEditScreen2", "显示加载指示器")
// 加载中状态 // 加载中状态
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
androidx.compose.material3.CircularProgressIndicator( androidx.compose.material3.CircularProgressIndicator(
@@ -327,24 +199,468 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
) )
} }
} }
model.profile != null -> {
Column(
modifier = Modifier.fillMaxSize()
) {
// 顶部背景区域(圆角在底部,覆盖状态栏)
// 优先显示新选择的背景图,如果没有则显示原有的背景图
val banner = model.bannerImageUrl?.toString() ?: model.profile?.banner
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
Box(
modifier = Modifier
.fillMaxWidth()
.offset(y = -statusBarPadding)
) {
Box(
modifier = Modifier
.width(402.dp)
.height(206.dp)
.align(Alignment.TopCenter)
.clip(RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp))
) {
if (banner != null) {
CustomAsyncImage(
context = context,
imageUrl = banner,
modifier = Modifier.fillMaxSize(),
contentDescription = "Banner",
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.2f))
)
}
// 更换封面按钮(位于壁纸右下方)
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 20.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color(0x5C7D7C80)) // RGB(125, 120, 128, 0.36)
.padding(horizontal = 8.dp, vertical = 4.dp)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// 更换封面图标
Icon(
painter = painterResource(id = R.mipmap.fengm),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color.White
)
Text(
text = stringResource(R.string.change_cover),
fontSize = 12.sp,
color = Color.White
)
}
}
// 状态栏区域(时间、信号、电池)
// 这里使用系统状态栏,不单独实现
// 导航栏区域
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(statusBarPadding + 12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
) {
// 返回按钮
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.3f))
.noRippleClickable {
navController.navigateUp()
}
.align(Alignment.CenterStart),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White)
)
}
// 标题
Text(
text = stringResource(R.string.edit_profile_info),
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
// 用户头像区域(距离顶部-50dp包含状态栏高度左右居中
Box(
modifier = Modifier
.fillMaxWidth()
.offset(y = (-50).dp - statusBarPadding),
contentAlignment = Alignment.Center
) {
val it = model.profile!!
Box(
modifier = Modifier.size(96.dp),
contentAlignment = Alignment.BottomEnd
) {
// 头像
CustomAsyncImage(
context,
model.croppedBitmap ?: it.avatar,
modifier = Modifier
.size(96.dp)
.clip(CircleShape)
.border(2.4.dp, appColors.background, CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
// 编辑头像按钮
Box(
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.background(Color(0xFF110C13))
.border(2.dp, Color.White, CircleShape)
.debouncedClickable(debounceTime = 800L) {
debouncedNavigation {
navController.navigate(NavigationRoute.ImageCrop.route)
}
},
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.mipmap.bi),
contentDescription = "Edit Avatar",
modifier = Modifier.size(16.dp),
tint = Color.White
)
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 16.dp)
.offset(y = (-74).dp)//
) {
// 昵称输入框
ProfileInfoCard(
label = stringResource(R.string.nickname),
value = model.name,
placeholder = "Value",
onValueChange = { onNicknameChange(it) },
isMultiline = false
)
Spacer(modifier = Modifier.height(16.dp))
// 个人简介输入框
ProfileInfoCard(
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) },
isMultiline = true
)
Spacer(modifier = Modifier.height(16.dp))
// MBTI 类型和星座
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(appColors.secondaryBackground)
) {
// MBTI 类型
ProfileSelectItem(
label = stringResource(R.string.mbti_type),
value = model.mbti ?: "ENFP",
iconColor = Color(0xFF7C45ED),
iconResDark = null, // TODO: 添加MBTI暗色模式图标
iconResLight = null, // TODO: 添加MBTI亮色模式图标
onClick = {
debouncedNavigation {
navController.navigate(NavigationRoute.MbtiSelect.route)
}
}
)
// 分隔线
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.3.dp)
.background(appColors.divider)
.padding(horizontal = 16.dp)
)
// 星座(使用当前图标)
ProfileSelectItem(
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, // 星座亮色模式图标
onClick = {
debouncedNavigation {
navController.navigate(NavigationRoute.ZodiacSelect.route)
}
}
)
}
Spacer(modifier = Modifier.weight(1f))
// 保存按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.clip(RoundedCornerShape(1000.dp))
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xFF7C45ED), // RGB(124, 69, 237) - 左侧
Color(0xFF7C57EE), // RGB(124, 87, 238) - 中间
Color(0xFF7BD8F8) // RGB(123, 216, 248) - 右侧
)
)
)
.debouncedClickable(
enabled = !model.isUpdating,
debounceTime = 1000L
) {
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 = stringResource(R.string.save),
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.White
)
}
}
}
}
else -> { else -> {
Log.d("AccountEditScreen2", "显示错误信息 - 没有数据且不在加载中")
// 没有数据且不在加载中,显示错误信息 // 没有数据且不在加载中,显示错误信息
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
androidx.compose.material3.Text( Text(
text = "加载用户资料失败,请重试", text = stringResource(R.string.error_load_profile_failed),
color = appColors.text color = appColors.text
) )
} }
} }
} }
}
}} }
}
/**
* 信息输入卡片组件
*/
@Composable
fun ProfileInfoCard(
label: String,
value: String,
placeholder: String,
onValueChange: (String) -> Unit,
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(appColors.secondaryBackground),
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
) {
// 标签
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text,
modifier = Modifier.width(100.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// 输入框
Box(
modifier = Modifier.weight(1f)
) {
if (value.isEmpty()) {
Text(
text = placeholder,
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth()
)
}
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
),
cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
singleLine = !isMultiline
)
}
}
}
}
/**
* 选择项组件MBTI、星座
*/
@Composable
fun ProfileSelectItem(
label: String,
value: String,
iconColor: Color,
onClick: () -> Unit,
iconResDark: Int? = null,
iconResLight: Int? = null
) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// 自定义图标
Icon(
painter = painterResource(
id = if (AppState.darkMode) {
// 暗色模式下使用和亮色模式一样的图标
iconResLight ?: iconResDark ?: R.mipmap.naoz
} else {
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
}
),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = iconColor
)
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = value,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.secondaryText
)
Icon(
imageVector = Icons.Default.ArrowForward,
contentDescription = null,
modifier = Modifier.size(8.dp),
tint = appColors.secondaryText
)
}
}
} }

View File

@@ -10,6 +10,7 @@ import androidx.compose.animation.togetherWith
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
@@ -36,6 +37,13 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
) )
} }
) { targetCount -> ) { 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

@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,7 +33,7 @@ fun FollowButton(
.wrapContentWidth() .wrapContentWidth()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background( .background(
color = if (isFollowing) AppColors.main else AppColors.nonActive color = if (isFollowing) AppColors.nonActive else AppColors.nonActive
) )
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable { .noRippleClickable {
@@ -41,11 +42,9 @@ fun FollowButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (isFollowing) stringResource(R.string.following_upper) else stringResource( text = if (isFollowing) stringResource(R.string.follow_upper_had) else stringResource(R.string.follow_upper),
R.string.follow_upper
),
fontSize = fontSize, fontSize = fontSize,
color = if (isFollowing) AppColors.mainText else AppColors.text, color = if (isFollowing) AppColors.text else AppColors.text,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
} }

View File

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

View File

@@ -43,6 +43,10 @@ import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
private val LabelTextColor = Color(red = 60f / 255f, green = 60f / 255f, blue = 67f / 255f, alpha = 0.6f)
private val HintTextColor = Color(red = 60f / 255f, green = 60f / 255f, blue = 67f / 255f, alpha = 0.3f)
private val PasswordIconColor = Color(red = 17f / 255f, green = 12f / 255f, blue = 19f / 255f)
@Composable @Composable
fun TextInputField( fun TextInputField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -52,69 +56,96 @@ fun TextInputField(
label: String? = null, label: String? = null,
hint: String? = null, hint: String? = null,
error: String? = null, error: String? = null,
enabled: Boolean = true enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null,
customBackgroundColor: Color? = null,
customCornerRadius: Float = 24f
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var showPassword by remember { mutableStateOf(!password) } var showPassword by remember { mutableStateOf(!password) }
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val backgroundColor = customBackgroundColor ?: AppColors.inputBackground
Column(modifier = modifier) { Column(modifier = modifier) {
label?.let { label?.let {
Text(it, color = AppColors.secondaryText) Text(
Spacer(modifier = Modifier.height(16.dp)) text = it,
color = LabelTextColor,
fontSize = 13.sp,
modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
)
} }
Box( Box(
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(customCornerRadius.dp))
.background(AppColors.inputBackground) .background(backgroundColor)
.border( .border(
width = 2.dp, width = 2.dp,
color = if (error == null) Color.Transparent else AppColors.error, color = if (error == null) Color.Transparent else AppColors.error,
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(customCornerRadius.dp)
) )
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically){ Row(
BasicTextField( verticalAlignment = Alignment.CenterVertically,
value = text, modifier = Modifier.fillMaxWidth()
onValueChange = onValueChange, ){
modifier = Modifier leadingIcon?.let {
.weight(1f) Box(modifier = Modifier.size(24.dp)) {
.onFocusChanged { focusState -> it()
isFocused = focusState.isFocused }
}, Spacer(modifier = Modifier.size(12.dp))
textStyle = TextStyle( }
fontSize = 16.sp, Box(modifier = Modifier.weight(1f)) {
fontWeight = FontWeight.W500, BasicTextField(
color = AppColors.text value = text,
), onValueChange = onValueChange,
keyboardOptions = KeyboardOptions( modifier = Modifier
keyboardType = if (password) KeyboardType.Password else KeyboardType.Text .fillMaxWidth()
), .onFocusChanged { focusState ->
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), isFocused = focusState.isFocused
singleLine = true, },
enabled = enabled, textStyle = TextStyle(
cursorBrush = SolidColor(AppColors.text), fontSize = 16.sp,
) fontWeight = FontWeight.W400,
color = AppColors.text
),
keyboardOptions = KeyboardOptions(
keyboardType = if (password) KeyboardType.Password else KeyboardType.Email
),
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
singleLine = true,
enabled = enabled,
cursorBrush = SolidColor(AppColors.text),
)
if (text.isEmpty() && hint != null) {
Text(
text = hint,
color = HintTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.W400
)
}
}
if (password) { if (password) {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_eye), painter = painterResource(
id = if (showPassword) {
R.drawable.rider_pro_eye
} else {
R.mipmap.icon_eyes_closed_light
}
),
contentDescription = "Password", contentDescription = "Password",
modifier = Modifier modifier = Modifier
.size(18.dp) .size(24.dp)
.noRippleClickable { .noRippleClickable {
showPassword = !showPassword showPassword = !showPassword
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(PasswordIconColor)
) )
} }
} }
if (text.isEmpty()) {
hint?.let {
Text(it, modifier = Modifier.padding(start = 5.dp), color = AppColors.inputHint, fontWeight = FontWeight.W600)
}
}
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -131,17 +133,25 @@ fun FollowerListScreen(userId: Int) {
) )
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注你呢", text = stringResource(R.string.follower_empty_title),
color = appColors.text, color = appColors.text,
fontSize = 16.sp, 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)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "试着发信号出来,某人就会被吸引啦~", text = stringResource(R.string.follower_empty_subtitle),
color = appColors.text, color = appColors.text,
fontSize = 14.sp, 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
@@ -122,17 +124,25 @@ fun FollowerNoticeScreen() {
) )
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注你呢", text = stringResource(R.string.follower_empty_title),
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, 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)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "试着发信号出来,某人就会被吸引啦~", text = stringResource(R.string.follower_empty_subtitle),
color = AppColors.text, color = AppColors.text,
fontSize = 14.sp, 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -133,17 +135,25 @@ fun FollowingListScreen(userId: Int) {
) )
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有关注任何灵魂", text = stringResource(R.string.following_empty_title),
color = appColors.text, color = appColors.text,
fontSize = 16.sp, 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)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "探索一下,总有一个你想靠近的光点 ✨", text = stringResource(R.string.following_empty_subtitle),
color = appColors.secondaryText, color = appColors.secondaryText,
fontSize = 14.sp, 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

@@ -9,9 +9,10 @@ import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ChatState import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CreatePromptRuleRequestBody import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.PromptRule import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.PromptRuleQuota import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.data.parseErrorResponse import com.aiosman.ravenow.data.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo import com.aiosman.ravenow.entity.GroupInfo
@@ -33,8 +34,8 @@ class GroupChatInfoViewModel(
val notificationStrategy get() = chatNotification?.strategy ?: "default" val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 记忆管理相关状态 // 记忆管理相关状态
var memoryQuota by mutableStateOf<PromptRuleQuota?>(null) var memoryQuota by mutableStateOf<AgentRuleQuota?>(null)
var memoryList by mutableStateOf<List<PromptRule>>(emptyList()) var memoryList by mutableStateOf<List<AgentRule>>(emptyList())
var isLoadingMemory by mutableStateOf(false) var isLoadingMemory by mutableStateOf(false)
var memoryError by mutableStateOf<String?>(null) var memoryError by mutableStateOf<String?>(null)
var promptOpenId by mutableStateOf<String?>(null) var promptOpenId by mutableStateOf<String?>(null)
@@ -137,12 +138,12 @@ class GroupChatInfoViewModel(
} }
// 创建智能体规则(群记忆) // 创建智能体规则(群记忆)
val requestBody = CreatePromptRuleRequestBody( val requestBody = CreateAgentRuleRequestBody(
rule = memoryText, rule = memoryText,
openId = openId openId = openId
) )
val response = ApiClient.api.createPromptRule(requestBody) val response = ApiClient.api.createAgentRule(requestBody)
if (response.isSuccessful) { if (response.isSuccessful) {
addMemorySuccess = true addMemorySuccess = true
@@ -184,14 +185,14 @@ class GroupChatInfoViewModel(
?: throw Exception("无法获取智能体信息") ?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId promptOpenId = fetchedOpenId
val quotaResponse = ApiClient.api.getPromptRuleQuota(fetchedOpenId) val quotaResponse = ApiClient.api.getAgentRuleQuota(fetchedOpenId)
if (quotaResponse.isSuccessful) { if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data memoryQuota = quotaResponse.body()?.data
} else { } else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}") throw Exception("获取配额信息失败: ${quotaResponse.code()}")
} }
} else { } else {
val quotaResponse = ApiClient.api.getPromptRuleQuota(targetOpenId) val quotaResponse = ApiClient.api.getAgentRuleQuota(targetOpenId)
if (quotaResponse.isSuccessful) { if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data memoryQuota = quotaResponse.body()?.data
} else { } else {
@@ -226,14 +227,14 @@ class GroupChatInfoViewModel(
?: throw Exception("无法获取智能体信息") ?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId promptOpenId = fetchedOpenId
val listResponse = ApiClient.api.getPromptRuleList(fetchedOpenId, page = page, pageSize = pageSize) val listResponse = ApiClient.api.getAgentRuleList(fetchedOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) { if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList() memoryList = listResponse.body()?.data?.list ?: emptyList()
} else { } else {
throw Exception("获取记忆列表失败: ${listResponse.code()}") throw Exception("获取记忆列表失败: ${listResponse.code()}")
} }
} else { } else {
val listResponse = ApiClient.api.getPromptRuleList(targetOpenId, page = page, pageSize = pageSize) val listResponse = ApiClient.api.getAgentRuleList(targetOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) { if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList() memoryList = listResponse.body()?.data?.list ?: emptyList()
} else { } else {
@@ -258,7 +259,7 @@ class GroupChatInfoViewModel(
isLoadingMemory = true isLoadingMemory = true
memoryError = null memoryError = null
val response = ApiClient.api.deletePromptRule(ruleId) val response = ApiClient.api.deleteAgentRule(ruleId)
if (response.isSuccessful) { if (response.isSuccessful) {
// 刷新记忆列表和配额 // 刷新记忆列表和配额
promptOpenId?.let { openId -> promptOpenId?.let { openId ->
@@ -292,13 +293,13 @@ class GroupChatInfoViewModel(
val openId = targetOpenId ?: promptOpenId val openId = targetOpenId ?: promptOpenId
?: throw Exception("无法获取智能体ID") ?: throw Exception("无法获取智能体ID")
val requestBody = com.aiosman.ravenow.data.api.UpdatePromptRuleRequestBody( val requestBody = UpdateAgentRuleRequestBody(
id = ruleId, id = ruleId,
rule = newRuleText, rule = newRuleText,
openId = openId openId = openId
) )
val response = ApiClient.api.updatePromptRule(requestBody) val response = ApiClient.api.updateAgentRule(requestBody)
if (response.isSuccessful) { if (response.isSuccessful) {
// 刷新记忆列表和配额 // 刷新记忆列表和配额
loadMemoryQuota(openId) loadMemoryQuota(openId)

View File

@@ -257,7 +257,7 @@ fun GroupMemoryManageContent(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EditGroupMemoryDialog( fun EditGroupMemoryDialog(
memory: com.aiosman.ravenow.data.api.PromptRule, memory: com.aiosman.ravenow.data.api.AgentRule,
viewModel: GroupChatInfoViewModel, viewModel: GroupChatInfoViewModel,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onUpdateMemory: (String) -> Unit onUpdateMemory: (String) -> Unit
@@ -403,7 +403,7 @@ fun EditGroupMemoryDialog(
*/ */
@Composable @Composable
fun MemoryItem( fun MemoryItem(
memory: com.aiosman.ravenow.data.api.PromptRule, memory: com.aiosman.ravenow.data.api.AgentRule,
isEditing: Boolean = false, isEditing: Boolean = false,
onEdit: () -> Unit = {}, onEdit: () -> Unit = {},
onCancel: () -> Unit = {}, onCancel: () -> Unit = {},

View File

@@ -1,6 +1,12 @@
package com.aiosman.ravenow.ui.index package com.aiosman.ravenow.ui.index
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -11,15 +17,21 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
@@ -42,8 +54,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@@ -59,7 +73,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.GuestLoginCheckOut import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene import com.aiosman.ravenow.GuestLoginCheckOutScene
@@ -123,151 +141,16 @@ fun IndexScreen() {
gesturesEnabled = drawerState.isOpen, gesturesEnabled = drawerState.isOpen,
drawerContent = { drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column( SideMenuContent(
modifier = Modifier onClose = {
.requiredWidth(250.dp) coroutineScope.launch {
.fillMaxHeight() drawerState.close()
.background(
AppColors.background
)
) {
Spacer(modifier = Modifier.height(88.dp))
NavItem(
iconRes = R.drawable.rave_now_nav_account,
label = stringResource(R.string.account_and_security),
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.AccountSetting.route)
}
} }
) },
Spacer(modifier = Modifier.height(16.dp)) navController = navController,
NavItem( context = context,
iconRes = R.drawable.rider_pro_favourited, isDrawerOpen = drawerState.isOpen
label = stringResource(R.string.favourites), )
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
)
NavItem(
iconRes = R.drawable.rave_now_nav_night,
label = stringResource(R.string.dark_mode),
rightContent = {
Switch(
checked = AppState.darkMode,
onCheckedChange = {
AppState.switchTheme()
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = AppColors.main,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = AppColors.main.copy(alpha = 0.5f),
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier.scale(0.8f)
)
}
)
// divider
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.divider)
)
}
NavItem(
iconRes = R.drawable.rave_now_nav_about,
label = stringResource(R.string.blocked),
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.AboutScreen.route)
}
}
)
NavItem(
iconRes = R.drawable.rave_now_nav_about,
label = stringResource(R.string.feedback),
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.AboutScreen.route)
}
}
)
NavItem(
iconRes = R.drawable.rave_now_nav_about,
label = stringResource(R.string.about_rave_now),
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.AboutScreen.route)
}
}
)
// divider
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.divider)
)
}
// NavItem(
// iconRes = R.drawable.rave_now_nav_switch,
// label = "Switch Account"
// )
// Spacer(modifier = Modifier.height(16.dp))
NavItem(
iconRes = R.drawable.rave_now_nav_logout,
label = stringResource(R.string.logout),
modifier = Modifier.noRippleClickable {
coroutineScope.launch {
drawerState.close()
// 只有非游客用户才需要取消注册推送设备
if (!AppStore.isGuest) {
Messaging.unregisterDevice(context)
}
AppStore.apply {
token = null
rememberMe = false
isGuest = false // 清除游客状态
saveData()
}
// 删除推送渠道
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Login.route) {
inclusive = true
}
}
AppState.ReloadAppState(context)
}
}
)
}
} }
} }
) { ) {
@@ -447,7 +330,6 @@ fun IndexScreen() {
) )
} }
} }
} }
@Composable @Composable
@@ -624,3 +506,369 @@ fun NavItem(
} }
} }
@Composable
fun SideMenuContent(
onClose: () -> Unit,
navController: androidx.navigation.NavController,
context: android.content.Context,
isDrawerOpen: Boolean
) {
val appColors = LocalAppTheme.current
val coroutineScope = rememberCoroutineScope()
var messageNotificationEnabled by remember { mutableStateOf(true) }
var darkModeEnabled by remember { mutableStateOf(AppState.darkMode) }
// 同步暗色模式状态
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 = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景
} else {
Color.White // 亮色模式:白色
}
// 文字颜色 - 根据暗色模式适配
val textColor = appColors.text
// 图标颜色 - 根据暗色模式适配
val iconColor = appColors.text
// 跟随系统文字颜色 - 根据暗色模式适配
val followSystemTextColor = appColors.secondaryText
// 开关开启颜色 #7C45ED
val switchActiveColor = Color(0xFF7C45ED)
Box(
modifier = Modifier
.fillMaxSize()
) {
// 左侧半透明遮罩(平滑淡入淡出)
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
val overlayAlpha by overlayTransition.animateFloat(
transitionSpec = {
if (targetState) {
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
} else {
tween(durationMillis = 300, easing = FastOutLinearInEasing)
}
},
label = "overlayAlpha"
) { open -> if (open) 0.6f else 0f }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = overlayAlpha))
)
// 右侧菜单面板
Box(
modifier = Modifier
.requiredWidth(302.dp)
.requiredHeight(874.dp)
.align(Alignment.CenterEnd)
.background(menuBackgroundColor)
) {
// 顶部状态栏间距
val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
// 扫一扫功能入口 - 右边距离右边66pt
Row(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-112).dp, y = 88.dp)
.noRippleClickable {
// TODO: 实现扫一扫功能
},
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 扫一扫图标(使用现有图标或占位)
Image(
painter = painterResource(id = R.mipmap.sao),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(iconColor)
)
}
// 绝对定位的"扫一扫"文字上方71.5dp右侧66dp
Text(
text = stringResource(R.string.scan_qr),
fontSize = 14.sp,
color = textColor,
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-66).dp, y = 91.5.dp)
)
// QR码图标 - 右边距离右边112dp上边距离上边68pt
Image(
painter = painterResource(id = R.mipmap.qr_code_icon),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.align(Alignment.TopEnd)
.offset(x = (-26).dp, y = 88.dp)
.noRippleClickable {
// TODO: 实现QR码功能
},
colorFilter = ColorFilter.tint(iconColor)
)
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt绝对定位
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = 128.dp) // 直接距离顶部128dp整体下移20dp
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 第一组卡片:编辑资料、账号安全、收藏
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 164.dp,
items = listOf(
MenuItem(
icon = R.mipmap.icons_edited_data,
label = stringResource(R.string.edit_profile_info),
onClick = {
coroutineScope.launch {
onClose()
navController.navigate(NavigationRoute.AccountEdit.route)
}
}
),
MenuItem(
icon = R.mipmap.icons_account_and_security,
label = stringResource(R.string.account_and_security),
onClick = {
coroutineScope.launch {
onClose()
navController.navigate(NavigationRoute.AccountSetting.route)
}
}
),
MenuItem(
icon = R.mipmap.collect,
label = stringResource(R.string.favourites),
onClick = {
coroutineScope.launch {
onClose()
navController.navigate(NavigationRoute.FavouriteList.route)
}
}
)
)
)
// 第二组卡片:暗色模式、消息通知
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 112.dp, // 根据设计图第二组卡片高度为112dp
items = listOf(
MenuItem(
icon = R.mipmap.icons_dark_mode,
label = stringResource(R.string.dark_mode),
rightContent = {
Switch(
checked = darkModeEnabled,
onCheckedChange = {
darkModeEnabled = it
AppState.darkMode = it
AppState.appTheme = if (it) {
com.aiosman.ravenow.DarkThemeColors()
} else {
com.aiosman.ravenow.LightThemeColors()
}
AppStore.saveDarkMode(it)
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = switchActiveColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = switchActiveColor.copy(alpha = 0.5f),
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier.size(width = 64.dp, height = 28.dp)
)
}
),
MenuItem(
icon = R.mipmap.icons_bell,
label = stringResource(R.string.message_notification),
rightContent = {
Switch(
checked = messageNotificationEnabled,
onCheckedChange = { messageNotificationEnabled = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = switchActiveColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = switchActiveColor.copy(alpha = 0.5f),
uncheckedBorderColor = Color.Transparent
),
modifier = Modifier.size(width = 64.dp, height = 28.dp)
)
}
)
)
)
// 第三组卡片:关于派派、反馈、退出登录
MenuCard(
backgroundColor = cardBackgroundColor,
textColor = textColor,
iconColor = iconColor,
width = 270.dp,
height = 164.dp,
items = listOf(
MenuItem(
icon = R.mipmap.icons_about,
label = stringResource(R.string.about_paipai),
onClick = {
coroutineScope.launch {
onClose()
navController.navigate(NavigationRoute.AboutScreen.route)
}
}
),
MenuItem(
icon = R.mipmap.feedback_icon,
label = stringResource(R.string.feedback),
onClick = {
coroutineScope.launch {
onClose()
navController.navigate(NavigationRoute.AboutScreen.route)
}
}
),
MenuItem(
icon = R.mipmap.log_out_icon,
label = stringResource(R.string.logout_confirm),
onClick = {
coroutineScope.launch {
onClose()
// 只有非游客用户才需要取消注册推送设备
if (!AppStore.isGuest) {
Messaging.unregisterDevice(context)
}
AppStore.apply {
token = null
rememberMe = false
isGuest = false
saveData()
}
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Login.route) {
inclusive = true
}
}
AppState.ReloadAppState(context)
}
},
showRightArrow = false
)
)
)
}
}
}
}
data class MenuItem(
val icon: Int,
val label: String,
val onClick: (() -> Unit)? = null,
val rightContent: @Composable (() -> Unit)? = null,
val showRightArrow: Boolean = true
)
@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
) {
Column(
modifier = Modifier
.then(if (width != null) Modifier.requiredWidth(width) else Modifier.fillMaxWidth())
.then(if (height != null) Modifier.requiredHeight(height) else Modifier)
.background(backgroundColor, RoundedCornerShape(16.dp))
.padding(horizontal = 16.dp),
verticalArrangement = if (height != null) Arrangement.SpaceEvenly else Arrangement.spacedBy(8.dp) // 固定高度时均匀分布
) {
items.forEachIndexed { index, item ->
Box(
modifier = Modifier
.then(if (height != null) Modifier.weight(1f) else Modifier),
contentAlignment = Alignment.Center
) {
MenuItemRow(item = item, compact = height != null, textColor = textColor, iconColor = iconColor) // 传递颜色参数
}
}
}
}
@Composable
fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, iconColor: Color) {
val appColors = LocalAppTheme.current
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (item.onClick != null) {
Modifier.noRippleClickable { item.onClick?.invoke() }
} else {
Modifier
}
)
.padding(vertical = if (compact) 4.dp else 8.dp), // 紧凑模式下减少垂直padding
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Image(
painter = painterResource(id = item.icon),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(iconColor)
)
Text(
text = item.label,
fontSize = 14.sp,
color = textColor
)
}
if (item.rightContent != null) {
item.rightContent?.invoke()
} else if (item.showRightArrow) {
Image(
painter = painterResource(id = R.drawable.rave_now_nav_right),
contentDescription = null,
modifier = Modifier.size(24.dp),
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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -981,9 +983,13 @@ fun ChatRoomCard(
Text( Text(
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}", text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp, fontSize = 12.sp,
modifier = Modifier.alpha(0.6f), modifier = Modifier
.alpha(0.6f)
.weight(1f),
color = AppColors.text, 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

@@ -234,22 +234,24 @@ object AgentViewModel: ViewModel() {
try { try {
// 获取完整的语言标记(如 "zh-CN" // 获取完整的语言标记(如 "zh-CN"
val sysLang = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag() val fullLangTag = com.aiosman.ravenow.utils.Utils.getPreferredLanguageTag()
// 转换为后端支持的语言代码(仅支持 zh、cn、ja
val sysLang = convertToSupportedLangCode(fullLangTag)
val response = apiClient.getCategories( val response = apiClient.getCategories(
page = 1, page = 1,
pageSize = 100, pageSize = 100,
isActive = true, isActive = true,
withChildren = false, // withChildren = false,
withParent = false, // withParent = false,
withCount = true, // withCount = true,
hideEmpty = true, // hideEmpty = true,
lang = sysLang lang = sysLang
) )
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang") println("分类数据请求完成,响应成功: ${response.isSuccessful}, 原始语言标记: $fullLangTag, 转换后: $sysLang")
if (response.isSuccessful) { if (response.isSuccessful) {
val categoryList = response.body()?.list ?: emptyList() val categoryList = response.body()?.list ?: emptyList()
println("获取到 ${categoryList.size} 个分类") println("获取到 ${categoryList.size} 个分类")
// 使用当前语言获取翻译后的分类名称 // 使用转换后的语言代码获取翻译后的分类名称
categories = categoryList.map { category -> categories = categoryList.map { category ->
CategoryItem.fromCategoryTemplate(category, sysLang) CategoryItem.fromCategoryTemplate(category, sysLang)
} }
@@ -266,6 +268,24 @@ object AgentViewModel: ViewModel() {
} }
} }
/**
* 将完整的语言标记转换为后端支持的语言代码
* 后端仅支持: zh, cn, ja
*
* @param langTag 完整的语言标记,如 "zh-CN", "zh-TW", "ja-JP", "en-US" 等
* @return 后端支持的语言代码,默认返回 "zh"
*/
private fun convertToSupportedLangCode(langTag: String): String {
return when {
langTag.startsWith("zh", ignoreCase = true) -> "zh"
langTag.startsWith("ja", ignoreCase = true) -> "ja"
// 如果是中文相关的其他标记,也返回 zh
langTag.equals("cn", ignoreCase = true) -> "cn"
// 默认返回中文
else -> "zh"
}
}
fun loadAgentsByCategory(categoryId: Int) { fun loadAgentsByCategory(categoryId: Int) {
loadAgentData(categoryId) loadAgentData(categoryId)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@@ -122,10 +123,11 @@ open class BaseMomentModel :ViewModel(){
userService.unFollowUser(moment.authorId.toString()) userService.unFollowUser(moment.authorId.toString())
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false)) EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false))
} else { } else {
userService.followUser(moment.authorId.toString()) // 调用 FollowerNoticeViewModel.followUser() 实现与 FollowerNotice.kt 相同的效果
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, true)) // 该方法内部会调用 userService.followUser() 并发布 FollowChangeEvent 事件
FollowerNoticeViewModel.followUser(moment.authorId)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }

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.TabItem
import com.aiosman.ravenow.ui.composables.UnderlineTabItem import com.aiosman.ravenow.ui.composables.UnderlineTabItem
import com.aiosman.ravenow.ui.composables.rememberDebouncer 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 = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 现在有6个tab推荐、短视频、新闻、探索、关注、热门 // 根据登录状态设置标签页数量游客模式5个tab非游客模式6个tab
val tabCount = 6 val tabCount = if (AppStore.isGuest) 5 else 6
var pagerState = rememberPagerState { tabCount } var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
Column( Column(
@@ -173,14 +174,14 @@ fun MomentsList() {
} }
) )
} else { } else {
// 热门标签 (游客模式) // 热门标签 (游客模式) - 在游客模式下热门标签对应第3页
UnderlineTabItem( UnderlineTabItem(
text = stringResource(R.string.index_hot), text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 4, isSelected = pagerState.currentPage == 3,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { 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( UnderlineTabItem(
text = stringResource(R.string.tab_news), text = stringResource(R.string.tab_news),
isSelected = pagerState.currentPage == 5, isSelected = pagerState.currentPage == newsPageIndex,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(5) pagerState.animateScrollToPage(newsPageIndex)
} }
} }
} }
@@ -234,6 +236,7 @@ fun MomentsList() {
} }
1 -> { 1 -> {
// 短视频页面 // 短视频页面
ShortVideoScreen()
} }
2 -> { 2 -> {
// 动态页面 - 暂时显示时间线内容 // 动态页面 - 暂时显示时间线内容
@@ -248,12 +251,18 @@ fun MomentsList() {
} }
} }
4 -> { 4 -> {
// 热门页面 (仅非游客用户) // 热门页面 (仅非游客用户) 或 新闻页面 (游客用户)
HotMomentsList() if (AppStore.isGuest) {
NewsScreen()
} else {
HotMomentsList()
}
} }
5 -> { 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.height 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.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
@@ -137,8 +139,18 @@ fun DiscoverView() {
val debouncer = rememberDebouncer() val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent 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 textLines = if (textContent.isNotEmpty()) {
val estimatedCharsPerLine = 20
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1 val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行 minOf(estimatedLines, 2) // 最多2行
} else { } else {
@@ -146,21 +158,20 @@ fun DiscoverView() {
} }
val baseHeight = 200.dp val baseHeight = 200.dp
val singleLineTextHeight = 20.dp val singleLineTextHeight = 24.dp // 增加高度以适应英文/日文
val doubleLineTextHeight = 40.dp val doubleLineTextHeight = 44.dp // 增加高度以适应英文/日文
val authorInfoHeight = 25.dp val authorInfoHeight = 25.dp
val paddingHeight = 10.dp val paddingHeight = 10.dp
val paddingHeight2 =3.dp val paddingHeight2 = 3.dp
val totalHeight = baseHeight + when (textLines) { val totalHeight = baseHeight + when (textLines) {
0 -> authorInfoHeight + paddingHeight 0 -> authorInfoHeight + paddingHeight
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight 1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
else -> doubleLineTextHeight + authorInfoHeight +paddingHeight2 else -> doubleLineTextHeight + authorInfoHeight + paddingHeight2
} }
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(totalHeight)
.padding(2.dp) .padding(2.dp)
.noRippleClickable { .noRippleClickable {
debouncer { debouncer {
@@ -173,7 +184,9 @@ fun DiscoverView() {
} }
) { ) {
Column( Column(
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) modifier = Modifier
.fillMaxWidth()
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
) { ) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail, imageUrl = momentItem.images[0].thumbnail,
@@ -193,9 +206,9 @@ fun DiscoverView() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(totalHeight - baseHeight)
.padding(horizontal = 8.dp, vertical = 8.dp) .padding(horizontal = 8.dp, vertical = 8.dp)
) { ) {
// 文本内容区域,限制最大高度
if (momentItem.momentTextContent.isNotEmpty()) { if (momentItem.momentTextContent.isNotEmpty()) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = momentItem.momentTextContent, text = momentItem.momentTextContent,
@@ -203,13 +216,19 @@ fun DiscoverView() {
fontSize = 12.sp, fontSize = 12.sp,
color = AppColors.text, color = AppColors.text,
maxLines = 2, 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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 25.dp) // 最小高度确保完整显示,自适应避免被挤压
.padding(top = 5.dp), .padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -225,7 +244,9 @@ fun DiscoverView() {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = momentItem.nickname, text = momentItem.nickname,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier
.padding(start = 4.dp)
.weight(1f),
fontSize = 11.sp, fontSize = 11.sp,
color = AppColors.text.copy(alpha = 0.6f), color = AppColors.text.copy(alpha = 0.6f),
maxLines = 1, 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

@@ -7,6 +7,7 @@ import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -86,8 +87,8 @@ import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaff
import com.aiosman.ravenow.ui.index.IndexViewModel import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
import com.aiosman.ravenow.ui.post.MenuActionItem import com.aiosman.ravenow.ui.post.MenuActionItem
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -100,6 +101,13 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.border
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.graphics.Brush
import java.text.NumberFormat
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -121,7 +129,8 @@ fun ProfileV3(
postCount: Long? = null, // 新增参数用于传递帖子总数 postCount: Long? = null, // 新增参数用于传递帖子总数
) { ) {
val model = MyProfileViewModel val model = MyProfileViewModel
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 }) // Tabs: 动态、(可选)智能体、群聊
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 2 else 3 })
val enabled by remember { mutableStateOf(true) } val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -134,7 +143,8 @@ fun ProfileV3(
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
val bannerHeight = 400 val bannerWidth = 402
val bannerHeight = 206
val pickBannerImageLauncher = pickupAndCompressLauncher( val pickBannerImageLauncher = pickupAndCompressLauncher(
context, context,
scope, scope,
@@ -156,12 +166,13 @@ fun ProfileV3(
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val toolbarAlpha by remember { // 计算导航栏背景透明度根据滚动位置从0到1
val toolbarBackgroundAlpha by remember {
derivedStateOf { derivedStateOf {
if (!isSelf) { if (!isSelf) {
1f 1f
} else { } else {
val maxScroll = 500f // 最大滚动距离,可调整 val maxScroll = 600f // 增加最大滚动距离,让渐变更平缓
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f) val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
progress progress
} }
@@ -308,12 +319,19 @@ fun ProfileV3(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(bannerHeight.dp) .height(bannerHeight.dp)
.background(AppColors.profileBackground) .background(AppColors.profileBackground),
contentAlignment = Alignment.Center
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .width(bannerWidth.dp)
.height(bannerHeight.dp - 24.dp) .height(bannerHeight.dp)
.clip(
RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
)
)
.let { .let {
if (isSelf && isMain) { if (isSelf && isMain) {
it.noRippleClickable { it.noRippleClickable {
@@ -326,13 +344,6 @@ fun ProfileV3(
it it
} }
} }
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
),
)
) { ) {
CustomAsyncImage( CustomAsyncImage(
LocalContext.current, LocalContext.current,
@@ -347,6 +358,9 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(100.dp)) Spacer(modifier = Modifier.height(100.dp))
} }
// 壁纸下方间距
Spacer(modifier = Modifier.height(16.dp))
// 用户信息 // 用户信息
Box( Box(
modifier = Modifier modifier = Modifier
@@ -357,41 +371,34 @@ fun ProfileV3(
profile?.let { profile?.let {
UserItem( UserItem(
accountProfileEntity = it, accountProfileEntity = it,
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong() postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size.toLong(),
isSelf = isSelf,
onEditClick = {
navController.navigate(NavigationRoute.AccountEdit.route)
}
) )
} }
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
// 操作按钮 // 操作按钮(仅其他用户显示)
profile?.let { profile?.let {
Box( if (!isSelf && it.id != AppState.UserId) {
modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.padding(horizontal = 16.dp) .fillMaxWidth()
) { .padding(horizontal = 16.dp)
if (isSelf) { ) {
SelfProfileAction( OtherProfileAction(
onEditProfile = { it,
navController.navigate(NavigationRoute.AccountEdit.route) onFollow = {
onFollowClick()
}, },
onPremiumClick = { onChat = {
navController.navigate(NavigationRoute.VipSelPage.route) onChatClick()
} }
) )
} else {
if (it.id != AppState.UserId) {
OtherProfileAction(
it,
onFollow = {
onFollowClick()
},
onChat = {
onChatClick()
}
)
}
} }
} }
} }
@@ -445,39 +452,50 @@ fun ProfileV3(
pagerState = pagerState, pagerState = pagerState,
showAgentTab = !isAiAccount showAgentTab = !isAiAccount
) )
Spacer(modifier = Modifier.height(8.dp))
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(500.dp) // 固定滚动高度 modifier = Modifier.height(500.dp) // 固定滚动高度
) { idx -> ) { idx ->
when (idx) { when (idx) {
0 -> 0 -> GalleryGrid(moments = moments)
GalleryGrid(moments = moments) 1 -> {
1 -> if (!isAiAccount) {
UserAgentsList( UserAgentsList(
agents = agents, agents = agents,
onAgentClick = onAgentClick, onAgentClick = onAgentClick,
onAvatarClick = { agent -> onAvatarClick = { agent ->
// 导航到智能体个人主页需要通过openId获取用户ID // 导航到智能体个人主页需要通过openId获取用户ID
scope.launch { scope.launch {
try { try {
val userService = com.aiosman.ravenow.data.UserServiceImpl() val userService = com.aiosman.ravenow.data.UserServiceImpl()
val profile = userService.getUserProfileByOpenId(agent.openId) val profile = userService.getUserProfileByOpenId(agent.openId)
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString()) .replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true") .replace("{isAiAccount}", "true")
) )
} catch (e: Exception) { } catch (e: Exception) {
// 处理错误 // 处理错误
}
} }
} },
}, modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize() )
) } else {
GroupChatPlaceholder()
}
}
2 -> {
if (!isAiAccount) {
GroupChatPlaceholder()
}
}
} }
} }
} }
// 底部间距,增加滚动距离
Spacer(modifier = Modifier.height(100.dp))
} }
// 顶部导航栏 // 顶部导航栏
@@ -486,9 +504,13 @@ fun ProfileV3(
isSelf = isSelf, isSelf = isSelf,
profile = profile, profile = profile,
navController = navController, navController = navController,
alpha = toolbarAlpha, backgroundAlpha = toolbarBackgroundAlpha,
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
onMenuClick = { onMenuClick = {
showOtherUserMenu = true showOtherUserMenu = true
},
onShareClick = {
// TODO: 实现分享功能
} }
) )
@@ -562,6 +584,11 @@ fun ProfileV3(
} }
} }
@Composable
private fun GroupChatPlaceholder() {
GroupChatEmptyContent()
}
//顶部导航栏组件 //顶部导航栏组件
@Composable @Composable
fun TopNavigationBar( fun TopNavigationBar(
@@ -569,100 +596,201 @@ fun TopNavigationBar(
isSelf: Boolean, isSelf: Boolean,
profile: AccountProfileEntity?, profile: AccountProfileEntity?,
navController: androidx.navigation.NavController, navController: androidx.navigation.NavController,
alpha: Float, backgroundAlpha: Float,
onMenuClick: () -> Unit = {} interactionCount: Int = 0,
onMenuClick: () -> Unit = {},
onShareClick: () -> Unit = {}
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
// 根据背景透明度和暗色模式决定图标颜色
// 暗色模式下:图标始终为白色
// 亮色模式下根据背景透明度决定透明度为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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.graphicsLayer { this.alpha = alpha }
) { ) {
Column( val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val statusBarHeight = statusBarPadding.calculateTopPadding()
val navigationBarHeight = 56.dp // 增加导航栏高度,包括图标和额外空间
// 导航栏背景层,包括状态栏区域,根据滚动位置逐渐变白
val totalHeight = statusBarHeight + navigationBarHeight
val density = LocalDensity.current
val totalHeightPx = with(density) { totalHeight.toPx() }
// 根据滚动位置计算基础颜色从深色平滑过渡到白色透明度从初始值逐渐减到0
val baseColor = remember(backgroundAlpha) {
val smoothProgress = backgroundAlpha.coerceIn(0f, 1f)
if (AppState.darkMode) {
// 暗色模式下从黑色透明度0逐渐变为黑色透明度1
val alpha = smoothProgress.coerceIn(0f, 1f)
Color.Black.copy(alpha = alpha)
} else {
// 亮色模式:初始状态:半透明深色,让白色图标清晰可见
val initialDarkAlpha = 0.12f
// 使用平滑的插值函数,让整个过渡更自然
val easedProgress = smoothProgress * smoothProgress * (3f - 2f * smoothProgress) // smoothstep
// 颜色值:从黑色(0)平滑过渡到白色(1)
val colorValue = 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)
)
}
}
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(appColors.profileBackground) .height(totalHeight) // 状态栏高度 + 导航栏高度
.align(Alignment.TopCenter)
.background(
brush = Brush.verticalGradient(
colors = listOf(
baseColor, // 顶部保持基础颜色
baseColor, // 中间保持基础颜色
baseColor.copy(alpha = baseColor.alpha * 0.5f), // 底部过渡,逐渐变透明
Color.Transparent // 最底部完全透明
),
startY = 0f,
endY = totalHeightPx
)
)
)
// 功能按钮区域,图标和文字根据背景透明度改变颜色
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp) // 增加上下内边距
.align(Alignment.TopEnd)
.padding(top = statusBarHeight), // 从状态栏下方开始
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) { ) {
StatusBarSpacer() // 左侧:互动数据卡片
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .height(24.dp)
.padding(horizontal = 16.dp, vertical = 8.dp) .background(
color = Color.White.copy(alpha = 0.52f),
shape = RoundedCornerShape(16.dp)
)
.border(
width = 0.5.dp,
color = cardBorderColor, // 根据背景透明度改变边框颜色
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
// 互动图标
Image(
painter = painterResource(id = R.mipmap.paip_coin_img),
contentDescription = "互动",
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = numberFormat.format(interactionCount),
fontSize = 14.sp,
fontWeight = FontWeight.W500,
color = if (AppState.darkMode) Color.White else Color.Black, // 暗色模式下为白色,亮色模式下为黑色
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.width(16.dp))
// 中间:分享图标
Image(
painter = painterResource(id = R.mipmap.menu_icon),
contentDescription = "分享",
modifier = Modifier
.size(24.dp)
.noRippleClickable { .noRippleClickable {
onShareClick()
}, },
colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色
)
Spacer(modifier = Modifier.width(16.dp))
// 右侧:菜单图标
Image(
painter = painterResource(id = R.mipmap.menu_ico),
contentDescription = "菜单",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
if (isSelf && isMain) {
IndexViewModel.openDrawer = true
} else {
onMenuClick()
}
},
colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色
)
}
// 如果不是主页面,显示返回按钮和用户信息
if (!isMain) {
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
// 判断是否有背景图
val hasBanner = profile?.banner != null
Row(
modifier = Modifier
.align(Alignment.TopStart)
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = statusBarPadding.calculateTopPadding()),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (!isMain) { // 返回按钮:深色模式下为白色,亮色模式下为黑色
Image( val backButtonColor = if (AppState.darkMode) Color.White else Color.Black
painter = painterResource(id = R.drawable.rider_pro_back_icon), Image(
contentDescription = "Back", painter = painterResource(id = R.drawable.rider_pro_back_icon),
modifier = Modifier contentDescription = "Back",
.noRippleClickable { modifier = Modifier
navController.navigateUp() .noRippleClickable {
} navController.navigateUp()
.size(24.dp), }
colorFilter = ColorFilter.tint(appColors.text) .size(24.dp),
) colorFilter = ColorFilter.tint(backButtonColor) // 深色模式下为白色,亮色模式下为黑色
)
// 未设置背景的用户(自己的主页且没有背景图)显示昵称
if (isSelf && !hasBanner && profile != null) {
Spacer(modifier = Modifier.width(8.dp)) 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(
text = profile?.nickName ?: "", text = profile.nickName ?: "",
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
color = appColors.text color = backButtonColor // 深色模式下为白色,亮色模式下为黑色
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSelf && isMain) {
Box(
modifier = Modifier
.size(24.dp)
.padding(16.dp)
)
} else if (!isSelf) {
Box(
modifier = Modifier
.noRippleClickable {
onMenuClick()
}
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "菜单",
tint = appColors.text,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
if (isSelf && isMain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 32.dp, end = 16.dp)
.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = appColors.text
) )
} }
} }
@@ -670,6 +798,77 @@ fun TopNavigationBar(
} }
} }
// 分享图标(向上箭头)
@Composable
fun ShareIcon(
color: Color,
modifier: Modifier = Modifier
) {
Canvas(modifier = modifier) {
val strokeWidth = 2.dp.toPx()
val centerX = size.width / 2
val centerY = size.height / 2
// 绘制向上的箭头
// 底部横线
drawLine(
color = color,
start = Offset(centerX - 9.dp.toPx(), centerY + 6.dp.toPx()),
end = Offset(centerX + 9.dp.toPx(), centerY + 6.dp.toPx()),
strokeWidth = strokeWidth
)
// 顶部横线
drawLine(
color = color,
start = Offset(centerX - 5.dp.toPx(), centerY - 6.5.dp.toPx()),
end = Offset(centerX + 5.dp.toPx(), centerY - 6.5.dp.toPx()),
strokeWidth = strokeWidth
)
// 中间竖线
drawLine(
color = color,
start = Offset(centerX, centerY - 3.dp.toPx()),
end = Offset(centerX, centerY + 6.dp.toPx()),
strokeWidth = strokeWidth
)
}
}
// 菜单图标(三条横线)
@Composable
fun MenuIcon(
color: Color,
modifier: Modifier = Modifier
) {
Canvas(modifier = modifier) {
val strokeWidth = 2.dp.toPx()
val centerX = size.width / 2
val centerY = size.height / 2
val lineLength = 16.dp.toPx()
val spacing = 6.dp.toPx()
// 绘制三条横线
drawLine(
color = color,
start = Offset(centerX - lineLength / 2, centerY - spacing),
end = Offset(centerX + lineLength / 2, centerY - spacing),
strokeWidth = strokeWidth
)
drawLine(
color = color,
start = Offset(centerX - lineLength / 2, centerY),
end = Offset(centerX + lineLength / 2, centerY),
strokeWidth = strokeWidth
)
drawLine(
color = color,
start = Offset(centerX - lineLength / 2, centerY + spacing),
end = Offset(centerX + lineLength / 2, centerY + spacing),
strokeWidth = strokeWidth
)
}
}
/** /**
* Agent菜单弹窗 * Agent菜单弹窗
*/ */

View File

@@ -42,6 +42,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource 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.AppState
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel 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)) Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "你的故事还没开始", text = stringResource(R.string.your_story_not_started),
fontSize = 16.sp, fontSize = 16.sp,
color = AppColors.text, 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)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "发布一条动态,和世界打个招呼吧", text = stringResource(R.string.publish_moment_greeting),
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
} else { } else {
@@ -221,7 +231,7 @@ fun GalleryGrid(
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp), modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
) { ) {
itemsIndexed(moments) { idx, moment -> itemsIndexed(moments) { idx, moment ->
if (moment != null) { if (moment != null && moment.images.isNotEmpty()) {
val itemDebouncer = rememberDebouncer() val itemDebouncer = rememberDebouncer()
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -0,0 +1,177 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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)
) {
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
SegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 空状态内容(居中)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 空状态插图
EmptyStateIllustration()
Spacer(modifier = Modifier.height(9.dp))
// 空状态文本
Text(
text = stringResource(R.string.empty_nothing),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = AppColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun SegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
// 全部
SegmentButton(
text = stringResource(R.string.chat_all),
isSelected = selectedIndex == 0,
onClick = { onSegmentSelected(0) },
width = 54.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 公开
SegmentButton(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 私有
SegmentButton(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp,
appColors = AppColors
)
}
}
@Composable
private fun SegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp,
appColors: com.aiosman.ravenow.AppThemeData
) {
Box(
modifier = Modifier
.width(width)
.height(32.dp)
.background(
color = if (isSelected) {
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
} else {
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
},
shape = RoundedCornerShape(1000.dp)
)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) {
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
} else {
appColors.text // 未选中时使用文本颜色
}
)
}
}
@Composable
private fun EmptyStateIllustration() {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "空状态",
modifier = Modifier
.width(181.dp)
.height(153.dp),
contentScale = ContentScale.Fit
)
}

View File

@@ -98,7 +98,7 @@ fun OtherProfileAction(
} }
) { ) {
Text( Text(
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper), text = if (profile.isFollowing) stringResource(R.string.follow_upper_had) else stringResource(R.string.follow_upper),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W900, fontWeight = FontWeight.W900,
color = if (profile.isFollowing) { color = if (profile.isFollowing) {

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -60,15 +61,14 @@ fun UserAgentsList(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
LazyColumn( if (agents.isEmpty()) {
modifier = modifier.fillMaxSize(), // 使用带分段控制器的空状态布局
verticalArrangement = Arrangement.spacedBy(8.dp) AgentEmptyContentWithSegments()
) { } else {
if (agents.isEmpty()) { LazyColumn(
item { modifier = modifier.fillMaxSize(),
EmptyAgentsView() verticalArrangement = Arrangement.spacedBy(8.dp)
} ) {
} else {
items(agents) { agent -> items(agents) { agent ->
UserAgentCard( UserAgentCard(
agent = agent, agent = agent,
@@ -76,11 +76,11 @@ fun UserAgentsList(
onAvatarClick = onAvatarClick onAvatarClick = onAvatarClick
) )
} }
}
// 底部间距 // 底部间距
item { item {
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
}
} }
} }
} }
@@ -198,6 +198,188 @@ fun UserAgentCard(
} }
} }
@Composable
fun AgentEmptyContentWithSegments() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
AgentSegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 空状态内容(使用智能体原本的图标和文字)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isNetworkAvailable) {
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai),
contentDescription = "暂无Agent",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
// 根据是否为深色模式调整间距
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.exclusive_ai_waiting),
fontSize = 16.sp,
color = AppColors.text,
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.ai_companion_not_tool),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
}
}
}
@Composable
private fun AgentSegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
// 全部
AgentSegmentButton(
text = stringResource(R.string.chat_all),
isSelected = selectedIndex == 0,
onClick = { onSegmentSelected(0) },
width = 54.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 公开
AgentSegmentButton(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 私有
AgentSegmentButton(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp,
appColors = AppColors
)
}
}
@Composable
private fun AgentSegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp,
appColors: com.aiosman.ravenow.AppThemeData
) {
Box(
modifier = Modifier
.width(width)
.height(32.dp)
.background(
color = if (isSelected) {
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
} else {
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
},
shape = RoundedCornerShape(1000.dp)
)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) {
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
} else {
appColors.text // 未选中时使用文本颜色
}
)
}
}
@Composable @Composable
fun EmptyAgentsView() { fun EmptyAgentsView() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current

View File

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

@@ -1,26 +1,39 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
@@ -29,23 +42,52 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import java.text.NumberFormat
import java.util.Locale
@Composable @Composable
fun UserItem( fun UserItem(
accountProfileEntity: AccountProfileEntity, accountProfileEntity: AccountProfileEntity,
postCount: Long = 0 postCount: Long = 0,
isSelf: Boolean = false,
onEditClick: () -> Unit = {}
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val followerDebouncer = rememberDebouncer() val followerDebouncer = rememberDebouncer()
val followingDebouncer = rememberDebouncer() val followingDebouncer = rememberDebouncer()
// 获取 MBTI 和星座信息
val mbti = remember(accountProfileEntity.id) {
AppStore.getUserMbti(accountProfileEntity.id)
}
val zodiac = remember(accountProfileEntity.id) {
AppStore.getUserZodiac(accountProfileEntity.id)
}
// 格式化粉丝数
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
val formattedFollowerCount = remember(accountProfileEntity.followerCount) {
if (accountProfileEntity.followerCount >= 10000) {
val wan = accountProfileEntity.followerCount / 10000.0
if (wan >= 100) {
"${wan.toInt()}"
} else {
String.format("%.1f万", wan)
}
} else {
numberFormat.format(accountProfileEntity.followerCount)
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
// 顶部:头像和统计数据
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// 头像 // 头像
CustomAsyncImage( CustomAsyncImage(
@@ -53,39 +95,47 @@ fun UserItem(
accountProfileEntity.avatar, accountProfileEntity.avatar,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(48.dp), .size(96.dp),
contentDescription = "", contentDescription = "",
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Spacer(modifier = Modifier.width(32.dp))
//个人统计 // 统计数据
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f) horizontalArrangement = Arrangement.spacedBy(0.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
// 帖子数 // 帖子数
Column( Column(
modifier = Modifier
.width(80.dp)
.height(40.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f) verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = postCount.toString(), text = postCount.toString(),
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "帖子", text = stringResource(R.string.posts),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
// 粉丝数 // 粉丝数
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.weight(1f) .width(80.dp)
.height(40.dp)
.noRippleClickable { .noRippleClickable {
followerDebouncer { followerDebouncer {
navController.navigate( navController.navigate(
@@ -95,26 +145,33 @@ fun UserItem(
) )
) )
} }
} },
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = accountProfileEntity.followerCount.toString(), text = formattedFollowerCount,
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "粉丝", text = stringResource(R.string.followers_upper),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
// 关注数 // 关注数
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.weight(1f) .width(80.dp)
.height(40.dp)
.offset(x = 6.dp)
.noRippleClickable { .noRippleClickable {
followingDebouncer { followingDebouncer {
navController.navigate( navController.navigate(
@@ -124,49 +181,161 @@ fun UserItem(
) )
) )
} }
} },
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = accountProfileEntity.followingCount.toString(), text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "关注", text = stringResource(R.string.following_upper),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
} }
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// 昵称
Text( // 中间:昵称、简介、创建者信息
text = accountProfileEntity.nickName, Column(
fontWeight = FontWeight.W600, modifier = Modifier
fontSize = 16.sp, .fillMaxWidth()
color = AppColors.text ) {
) // 昵称
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()){
Text( Text(
text = accountProfileEntity.bio, text = accountProfileEntity.nickName,
fontSize = 14.sp, fontWeight = FontWeight.Bold,
color = AppColors.secondaryText, fontSize = 22.sp,
maxLines = 1, letterSpacing = (-0.3).sp,
overflow = TextOverflow.Ellipsis color = AppColors.text
) )
}else{
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()) {
Text(
text = accountProfileEntity.bio,
fontSize = 13.sp,
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
Text(
text = "Welcome to my fantiac word i will show you something about magic",
fontSize = 13.sp,
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// 创建者信息(如果是 AI 账户,可以显示创建者)
// 注意:当前 AccountProfileEntity 没有创建者字段,这里暂时留空
// 如果需要显示,需要从其他地方获取创建者信息
}
Spacer(modifier = Modifier.height(12.dp))
// 底部:标签按钮
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// MBTI 标签
if (!mbti.isNullOrEmpty()) {
ProfileTag(
text = mbti,
backgroundColor = Color(0x33FF8D28), // 255/255, 141/255, 40/255, alpha 0.2
textColor = AppColors.text
)
}
// 星座标签
if (!zodiac.isNullOrEmpty()) {
ProfileTag(
text = zodiac,
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
textColor = AppColors.text
)
}
// 编辑标签(仅自己可见)
if (isSelf) {
ProfileTag(
text = stringResource(R.string.edit_profile),
backgroundColor = Color(0x14947A80), // 124/255, 116/255, 128/255, alpha 0.08
textColor = AppColors.text,
leadingIcon = {
EditIcon(
color = AppColors.text,
modifier = Modifier.size(16.dp)
)
},
onClick = onEditClick
)
}
}
}
}
@Composable
private fun ProfileTag(
text: String,
backgroundColor: Color,
textColor: Color,
leadingIcon: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null
) {
Box(
modifier = Modifier
.height(25.dp)
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
)
.padding(horizontal = 8.dp, vertical = 4.dp),
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
leadingIcon?.invoke()
Text( Text(
text = "No bio here.", text = text,
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = textColor
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
} }
}
@Composable
private fun EditIcon(
color: Color,
modifier: Modifier = Modifier
) {
Image(
painter = painterResource(id = R.mipmap.bi),
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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -61,27 +62,65 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.comment.CommentModalContent import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun ShortViewCompose( fun ShortViewCompose(
videoItemsUrl: List<String>, videoItemsUrl: List<String> = emptyList(),
videoMoments: List<MomentEntity> = emptyList(),
clickItemPosition: Int = 0, clickItemPosition: Int = 0,
videoHeader: @Composable () -> Unit = {}, 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 { // 优先使用 videoMoments如果没有则使用 videoItemsUrl
remember { val items = if (videoMoments.isNotEmpty()) {
PagerState(clickItemPosition, 0, videoItemsUrl.size - 1) 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 { val initialLayout = remember {
mutableStateOf(true) mutableStateOf(true)
@@ -89,20 +128,39 @@ fun ShortViewCompose(
val pauseIconVisibleState = remember { val pauseIconVisibleState = remember {
mutableStateOf(false) mutableStateOf(false)
} }
Pager( Pager(
modifier = Modifier
.fillMaxSize()
.clip(RectangleShape),
state = pagerState, state = pagerState,
orientation = Orientation.Vertical, orientation = Orientation.Vertical,
offscreenLimit = 1 offscreenLimit = 1
) { ) {
pauseIconVisibleState.value = false pauseIconVisibleState.value = false
val currentMoment = if (videoMoments.isNotEmpty() && page < videoMoments.size) {
videoMoments[page]
} else {
null
}
// 同步页码到外部(用于返回时恢复进度)
LaunchedEffect(pagerState.currentPage) {
onPageChanged?.invoke(pagerState.currentPage)
}
SingleVideoItemContent( SingleVideoItemContent(
videoItemsUrl[page], videoUrl = items[page],
pagerState, moment = currentMoment,
page, pagerState = pagerState,
initialLayout, pager = page,
pauseIconVisibleState, initialLayout = initialLayout,
videoHeader, pauseIconVisibleState = pauseIconVisibleState,
videoBottom VideoHeader = videoHeader,
VideoBottom = videoBottom,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onFavoriteClick = onFavoriteClick,
onShareClick = onShareClick
) )
} }
@@ -116,18 +174,39 @@ fun ShortViewCompose(
@Composable @Composable
private fun SingleVideoItemContent( private fun SingleVideoItemContent(
videoUrl: String, videoUrl: String,
moment: MomentEntity?,
pagerState: PagerState, pagerState: PagerState,
pager: Int, pager: Int,
initialLayout: MutableState<Boolean>, initialLayout: MutableState<Boolean>,
pauseIconVisibleState: MutableState<Boolean>, pauseIconVisibleState: MutableState<Boolean>,
VideoHeader: @Composable() () -> Unit, 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
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(
VideoPlayer(videoUrl, pagerState, pager, pauseIconVisibleState) 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() VideoHeader.invoke()
Box(modifier = Modifier.align(Alignment.BottomStart)) { if (moment != null && VideoBottom != null) {
VideoBottom.invoke() Box(modifier = Modifier.align(Alignment.BottomStart)) {
VideoBottom.invoke(moment)
}
} }
if (initialLayout.value) { if (initialLayout.value) {
Box( Box(
@@ -143,9 +222,14 @@ private fun SingleVideoItemContent(
@Composable @Composable
fun VideoPlayer( fun VideoPlayer(
videoUrl: String, videoUrl: String,
moment: MomentEntity?,
pagerState: PagerState, pagerState: PagerState,
pager: Int, pager: Int,
pauseIconVisibleState: MutableState<Boolean>, pauseIconVisibleState: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -158,9 +242,20 @@ fun VideoPlayer(
ExoPlayer.Builder(context) ExoPlayer.Builder(context)
.build() .build()
.apply { .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( val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory(
context, context,
Util.getUserAgent(context, context.packageName) httpDataSourceFactory
) )
val source = ProgressiveMediaSource.Factory(dataSourceFactory) val source = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl))) .createMediaSource(MediaItem.fromUri(Uri.parse(videoUrl)))
@@ -275,70 +370,107 @@ fun VideoPlayer(
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp), modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
UserAvatar() if (moment != null) {
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "975.9k") UserAvatar(avatarUrl = moment.avatar)
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "1896") { VideoBtn(
showCommentModal = true 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 // info
Box( if (moment != null) {
modifier = Modifier.fillMaxSize(), Box(
contentAlignment = Alignment.BottomStart modifier = Modifier.fillMaxSize(),
) { contentAlignment = Alignment.BottomStart
Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) { ) {
Row( Column(modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)) {
modifier = Modifier if (moment.location.isNotEmpty() && moment.location != "Worldwide") {
.padding(bottom = 8.dp) Row(
.background(color = Color.Gray), modifier = Modifier
verticalAlignment = Alignment.CenterVertically, .padding(bottom = 8.dp)
horizontalArrangement = Arrangement.Start, .background(color = Color.Gray),
) { verticalAlignment = Alignment.CenterVertically,
Image( horizontalArrangement = Arrangement.Start,
modifier = Modifier ) {
.size(20.dp) Image(
.padding(start = 4.dp, end = 6.dp), modifier = Modifier
painter = painterResource(id = R.drawable.rider_pro_video_location), .size(20.dp)
contentDescription = "" .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( Text(
modifier = Modifier.padding(end = 4.dp), text = "@${moment.nickname}",
text = "USA", fontSize = 16.sp,
fontSize = 12.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) 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( ModalBottomSheet(
onDismissRequest = { showCommentModal = false }, onDismissRequest = { showCommentModal = false },
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState sheetState = sheetState
) { ) {
CommentModalContent() { CommentModalContent(postId = moment.id) {
} }
} }
@@ -346,16 +478,37 @@ fun VideoPlayer(
} }
@Composable @Composable
fun UserAvatar() { fun UserAvatar(avatarUrl: String? = null) {
Image( Box(
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
.size(40.dp) .size(40.dp)
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp)) .border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
.clip( .clip(RoundedCornerShape(40.dp))
RoundedCornerShape(40.dp) ) {
), painter = painterResource(id = R.drawable.default_avatar), contentDescription = "" 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 @Composable

View File

@@ -1,15 +1,19 @@
package com.aiosman.ravenow.ui.login package com.aiosman.ravenow.ui.login
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -19,9 +23,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
@@ -33,25 +41,28 @@ import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.AccountServiceImpl import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.api.getErrorMessageCode import com.aiosman.ravenow.data.api.getErrorMessageCode
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.ActionButton import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.CheckboxWithLabel import com.aiosman.ravenow.ui.composables.CheckboxWithLabel
import com.aiosman.ravenow.ui.composables.PolicyCheckbox import com.aiosman.ravenow.ui.composables.PolicyCheckbox
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TextInputField import com.aiosman.ravenow.ui.composables.TextInputField
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.PasswordValidator import com.aiosman.ravenow.utils.PasswordValidator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private val LightGrayBackground = Color(red = 250f / 255f, green = 249f / 255f, blue = 251f / 255f)
private val IconGray = Color(red = 151f / 255f, green = 148f / 255f, blue = 153f / 255f)
private val PurpleButton = Color(0xFF7C45ED)
@Composable @Composable
fun EmailSignupScreen() { fun EmailSignupScreen() {
var appColor = LocalAppTheme.current val appColor = LocalAppTheme.current
var email by remember { mutableStateOf("") } var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") } var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) } var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) } var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
@@ -60,14 +71,13 @@ fun EmailSignupScreen() {
var passwordError by remember { mutableStateOf<String?>(null) } var passwordError by remember { mutableStateOf<String?>(null) }
var confirmPasswordError by remember { mutableStateOf<String?>(null) } var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var termsError by remember { mutableStateOf<Boolean>(false) } var termsError by remember { mutableStateOf<Boolean>(false) }
var promotionsError by remember { mutableStateOf<Boolean>(false) }
fun validateForm(): Boolean { fun validateForm(): Boolean {
emailError = when { emailError = when {
// 非空 // 非空
email.isEmpty() -> context.getString(R.string.text_error_email_required) email.isEmpty() -> context.getString(R.string.text_error_email_required)
// 邮箱格式 // 邮箱格式
!android.util.Patterns.EMAIL_ADDRESS.matcher(email) !android.util.Patterns.EMAIL_ADDRESS.matcher(email)
.matches() -> context.getString(R.string.text_error_email_format) .matches() -> context.getString(R.string.text_error_email_format_1)
else -> null else -> null
} }
@@ -88,22 +98,8 @@ fun EmailSignupScreen() {
} }
termsError = true termsError = true
return false return false
} else {
termsError = false
}
if (!acceptPromotions) {
scope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.error_not_accept_recive_notice),
Toast.LENGTH_SHORT
).show()
}
promotionsError = true
return false
} else {
promotionsError = false
} }
termsError = false
return emailError == null && passwordError == null && confirmPasswordError == null return emailError == null && passwordError == null && confirmPasswordError == null
} }
@@ -158,63 +154,127 @@ fun EmailSignupScreen() {
} }
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(appColor.background) .background(appColor.background)
) { ) {
StatusBarSpacer() StatusBarSpacer()
Box( // 顶部导航栏:返回箭头 + "注册"标题,左对齐
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, end = 16.dp) .padding(top = 15.dp, start = 16.dp, bottom = 15.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
NoticeScreenHeader(stringResource(R.string.sign_up_upper), moreIcon = false) Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateUp()
},
colorFilter = ColorFilter.tint(Color.Black)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.sign_up_upper),
fontSize = 20.sp,
fontWeight = FontWeight.W600,
color = Color.Black
)
} }
Spacer(modifier = Modifier.padding(32.dp))
// 输入区域
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.padding(horizontal = 24.dp) .padding(horizontal = 0.dp)
) { ) {
Spacer(modifier = Modifier.height(16.dp))
// 邮箱输入框
TextInputField( TextInputField(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.padding(horizontal = 24.dp),
text = email, text = email,
onValueChange = { onValueChange = {
email = it email = it
}, },
label = stringResource(R.string.login_email_label),
hint = stringResource(R.string.text_hint_email), hint = stringResource(R.string.text_hint_email),
error = emailError error = emailError,
leadingIcon = {
Image(
painter = painterResource(id = R.mipmap.icon_email_light),
contentDescription = "Email",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(IconGray)
)
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
) )
Spacer(modifier = Modifier.padding(4.dp))
// 密码输入框
TextInputField( TextInputField(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.padding(horizontal = 24.dp),
text = password, text = password,
onValueChange = { onValueChange = {
password = it password = it
}, },
password = true, password = true,
label = stringResource(R.string.text_hint_password).replace("输入", ""),
hint = stringResource(R.string.text_hint_password), hint = stringResource(R.string.text_hint_password),
error = passwordError error = passwordError,
leadingIcon = {
Image(
painter = painterResource(id = R.mipmap.icon_lock_light),
contentDescription = "Lock",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(IconGray)
)
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
) )
Spacer(modifier = Modifier.padding(4.dp))
// 确认密码输入框
TextInputField( TextInputField(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.padding(horizontal = 24.dp),
text = confirmPassword, text = confirmPassword,
onValueChange = { onValueChange = {
confirmPassword = it confirmPassword = it
}, },
password = true, password = true,
hint = stringResource(R.string.text_hint_confirm_password), label = stringResource(R.string.confirm_password_label),
error = confirmPasswordError hint = stringResource(R.string.text_hint_password),
error = confirmPasswordError,
leadingIcon = {
Image(
painter = painterResource(id = R.mipmap.icon_lock_light),
contentDescription = "Lock",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(IconGray)
)
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
) )
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(16.dp))
// 功能选项区域
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
) { ) {
CheckboxWithLabel( CheckboxWithLabel(
@@ -236,42 +296,31 @@ fun EmailSignupScreen() {
termsError = false termsError = false
} }
} }
Spacer(modifier = Modifier.height(16.dp))
CheckboxWithLabel(
checked = acceptPromotions,
checkSize = 16,
fontSize = 12,
label = stringResource(R.string.agree_promotion),
error = promotionsError
) {
acceptPromotions = it
// 当用户勾选时,立即清除错误状态
if (it) {
promotionsError = false
}
}
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(76.dp))
// 底部注册按钮
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ActionButton( ActionButton(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.width(345.dp), text = stringResource(R.string.sign_up_upper),
text = stringResource(R.string.lets_ride_upper), backgroundColor = PurpleButton,
backgroundColor = Color(0xffda3832), color = Color.White,
color = Color.White fontSize = 17.sp,
fontWeight = FontWeight.W600
) { ) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
registerUser() registerUser()
} }
} }
} }
} }
} }
} }

View File

@@ -72,8 +72,17 @@ fun UserAuthScreen() {
var passwordError by remember { mutableStateOf<String?>(null) } var passwordError by remember { mutableStateOf<String?>(null) }
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) } var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
fun validateForm(): Boolean { fun validateForm(): Boolean {
emailError = // 如果密码为空,先检查邮箱格式
if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null if (password.isEmpty()) {
emailError = when {
email.isEmpty() -> context.getString(R.string.text_error_email_required)
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() ->
context.getString(R.string.text_error_email_format)
else -> null
}
} else {
emailError = if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
}
// 使用通用密码校验器 // 使用通用密码校验器
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context) val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)

View File

@@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -52,6 +54,8 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
@@ -66,6 +70,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -151,71 +156,45 @@ fun NewPostScreen() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(1.dp) .height(1.dp)
.padding(horizontal = 16.dp)
.background(AppColors.divider) .background(AppColors.divider)
) )
Spacer(modifier = Modifier.height(24.dp))
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(start = 16.dp)
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp), .height(40.dp)
verticalAlignment = Alignment.CenterVertically .widthIn(min = 100.dp, max = 200.dp)
.wrapContentWidth()
.clip(RoundedCornerShape(20.dp))
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF8CDDFF),
Color(0xFF9887FF),
Color(0xFFFF8D28)
),
)
)
.padding(horizontal = 14.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) { ) {
Image( Image(
painter = painterResource(id = R.mipmap.rider_pro_moment_ai), painter = painterResource(id = R.mipmap.icon_ai),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(24.dp) .size(16.dp)
) )
Text( Text(
text = stringResource(R.string.moment_ai_co), text = stringResource(R.string.moment_ai_co),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Normal,
fontSize = 15.sp, fontSize = 13.sp,
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 2.dp),
.weight(1f), color = Color.White,
color = AppColors.text, maxLines = 1,
) overflow = TextOverflow.Ellipsis
Switch(
checked = isAiEnabled,
onCheckedChange = {
isChecked ->
isAiEnabled = isChecked
if (isChecked) {
// 收起键盘
keyboardController?.hide()
isRequesting = true
isRotating = true
model.viewModelScope.launch {
try {
model.agentMoment(model.textContent)
} catch (e: Exception) {
e.printStackTrace()
}finally {
isRequesting = false
isRotating = false
isAiEnabled = false
}
}
} else {
}
},
enabled = !isRequesting && model.textContent.isNotEmpty(),
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = AppColors.brandColorsColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = AppColors.nonActive,
uncheckedBorderColor = Color.White,
disabledCheckedTrackColor = AppColors.brandColorsColor.copy(alpha = 0.8f),
disabledCheckedThumbColor= Color.White,
disabledUncheckedTrackColor = AppColors.nonActive,
disabledUncheckedThumbColor= Color.White
),
modifier = Modifier.scale(0.8f)
) )
} }
@@ -352,7 +331,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
modifier = Modifier.align(Alignment.CenterStart), modifier = Modifier.align(Alignment.CenterStart),
) { ) {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_close), painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back", contentDescription = "Back",
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
@@ -366,9 +345,31 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
Spacer(modifier = Modifier.width(8.dp))
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = stringResource(R.string.publish_dynamic),
fontSize = 17.sp,
color = AppColors.text,
)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Image( Image(
painter = painterResource(id = R.mipmap.rider_pro_moment_post), painter = painterResource(id = R.mipmap.icon_draft_box_light),
contentDescription = "",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
// 添加防抖逻辑
val currentTime = System.currentTimeMillis()
if (currentTime - lastSendClickTime > debounceTime) {
lastSendClickTime = currentTime
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(20.dp))
Image(
painter = painterResource(id = R.mipmap.icon_released_light),
contentDescription = "Send", contentDescription = "Send",
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
@@ -391,11 +392,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}finally { }finally {
uploading = false uploading = false
} }
} }
} },
) )
} }
@@ -488,72 +486,24 @@ fun AddImageGrid() {
} }
val addImageDebouncer = rememberDebouncer() val addImageDebouncer = rememberDebouncer()
val takePhotoDebouncer = rememberDebouncer()
val stroke = Stroke(
width = 2f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
)
DraggableGrid(
items = NewPostViewModel.imageList,
onMove = { from, to ->
NewPostViewModel.imageList = NewPostViewModel.imageList.toMutableList().apply {
add(to, removeAt(from))
}
},
lockedIndices = listOf(
),
onDragModeEnd = {},
onDragModeStart = {},
additionalItems = listOf(
),
getItemId = { it.id }
) { item, isDrag ->
Box(
modifier = Modifier
) {
CustomAsyncImage(
LocalContext.current,
item.bitmap,
contentDescription = "Image",
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.noRippleClickable {
navController.navigate(NavigationRoute.NewPostImageGrid.route)
},
contentScale = ContentScale.Crop
)
if (isDrag) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.4f))
)
}
}
}
val canAddMoreImages = model.imageList.size < 9 val canAddMoreImages = model.imageList.size < 9
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(5), columns = GridCells.Fixed(5),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(horizontal = 19.dp, vertical = 4.dp),
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// 添加按钮
if (canAddMoreImages) { if (canAddMoreImages) {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)) // 设置圆角 .clip(RoundedCornerShape(24.dp))
.background(AppColors.basicMain) // 设置背景色 .background(Color(0xFFFAF9FB))
.noRippleClickable { .noRippleClickable {
addImageDebouncer { addImageDebouncer {
if (model.imageList.size < 9) { if (model.imageList.size < 9) {
@@ -572,20 +522,21 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = "Add Image", contentDescription = "Add Image",
modifier = Modifier modifier = Modifier
.size(24.dp) .size(23.3.dp)
.align(Alignment.Center), .align(Alignment.Center),
tint = AppColors.nonActiveText tint = AppColors.nonActiveText
) )
} }
} }
// 相机按钮
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)) // 设置圆角 .clip(RoundedCornerShape(24.dp))
.background(AppColors.basicMain) // 设置背景色 .background(Color(0xFFFAF9FB))
.noRippleClickable { .noRippleClickable {
if (model.imageList.size < 9) { if (model.imageList.size < 9) {
val photoFile = File(context.cacheDir, "photo.jpg") val photoFile = File(context.cacheDir, "photo.jpg")
@@ -605,13 +556,60 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_camera), painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "Take Photo", contentDescription = "Take Photo",
modifier = Modifier modifier = Modifier
.size(24.dp) .size(23.3.dp)
.align(Alignment.Center), .align(Alignment.Center),
tint = AppColors.nonActiveText tint = AppColors.nonActiveText
) )
} }
} }
} }
// 已添加的图片,显示在相机按钮后面
items(model.imageList.size) { index ->
val item = model.imageList[index]
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(24.dp))
.background(Color(0xFFFAF9FB))
) {
CustomAsyncImage(
context,
item.bitmap,
contentDescription = "Image",
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
navController.navigate(NavigationRoute.NewPostImageGrid.route)
},
contentScale = ContentScale.Crop
)
// // 删除按钮 - 右上角
// Box(
// modifier = Modifier
// .align(Alignment.TopEnd)
// .padding(4.dp)
// .size(20.dp)
// .clip(RoundedCornerShape(10.dp))
// .background(Color.Black.copy(alpha = 0.6f))
// .noRippleClickable {
// model.imageList = model.imageList.toMutableList().apply {
// removeAt(index)
// }
// },
// contentAlignment = Alignment.Center
// ) {
// Icon(
// painter = painterResource(id = R.drawable.rider_pro_close),
// contentDescription = "Delete",
// modifier = Modifier.size(12.dp),
// tint = Color.White
// )
// }
}
}
} }
} }

View File

@@ -13,6 +13,7 @@ import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.FollowChangeEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent import com.aiosman.ravenow.event.MomentRemoveEvent
@@ -166,7 +167,8 @@ class PostViewModel(
moment?.let { moment?.let {
userService.followUser(it.authorId.toString()) userService.followUser(it.authorId.toString())
moment = moment?.copy(followStatus = true) moment = moment?.copy(followStatus = true)
// 更新我的关注页面的关注数 // 发送关注事件,通知动态列表更新关注状态
EventBus.getDefault().post(FollowChangeEvent(it.authorId, true))
} }
} }
@@ -174,7 +176,8 @@ class PostViewModel(
moment?.let { moment?.let {
userService.unFollowUser(it.authorId.toString()) userService.unFollowUser(it.authorId.toString())
moment = moment?.copy(followStatus = false) moment = moment?.copy(followStatus = false)
// 更新我的关注页面的关注数 // 发送取消关注事件,通知动态列表更新关注状态
EventBus.getDefault().post(FollowChangeEvent(it.authorId, false))
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Some files were not shown because too many files have changed in this diff Show More