27 Commits

Author SHA1 Message Date
4a19d4c60e Reapply "增加英文日语翻译 修改编辑资料界面无法更改星座mbit"
This reverts commit a057f7f7fd.
2025-11-10 15:13:26 +08:00
a057f7f7fd Revert "增加英文日语翻译 修改编辑资料界面无法更改星座mbit"
This reverts commit f95c0204ee.
2025-11-10 14:59:06 +08:00
f95c0204ee 增加英文日语翻译 修改编辑资料界面无法更改星座mbit 2025-11-10 14:53:23 +08:00
c5156a5f6c Merge remote-tracking branch 'origin/main' into feat/pr-20251104-154907 2025-11-10 14:13:05 +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
de85f35c45 优化分类数据加载的语言参数
根据系统语言标签(如 "zh-CN")将其转换为后端支持的语言代码(zh, cn, ja),用于请求分类数据。默认回退到 "zh"。
2025-11-10 10:46:25 +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
153 changed files with 2538 additions and 956 deletions

View File

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

View File

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

View File

@@ -954,7 +954,7 @@ interface RaveNowAPI {
@Query("nickname") search: String? = null,
@Query("followerId") followerId: Int? = null,
@Query("followingId") followingId: Int? = null,
@Query("includeAI") includeAI: Boolean? = false,
@Query("includeAI") includeAI: Boolean? = true,
@Query("chatSessionIdNotNull") chatSessionIdNotNull: Boolean? = true,
): Response<ListContainer<AccountProfile>>

View File

@@ -1,117 +1,177 @@
package com.aiosman.ravenow.ui.account
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
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.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.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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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.LocalNavController
import com.aiosman.ravenow.Messaging
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.AccountServiceImpl
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.index.NavItem
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)
@Composable
fun AccountSetting() {
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.background),
) {
StatusBarSpacer()
// 顶部标题栏
Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
modifier = Modifier
.fillMaxWidth()
.padding(vertical = AccountSettingConstants.HEADER_VERTICAL_PADDING.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.account_and_security),
moreIcon = false
CircularBackButton(
onClick = { navController.navigateUp() },
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(
modifier = Modifier.padding(start = 24.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.background(
color = appColors.background,
shape = RoundedCornerShape(AccountSettingConstants.CARD_CORNER_RADIUS.dp)
)
) {
NavItem(
iconRes = R.mipmap.rider_pro_change_password,
SecurityOptionItem(
iconRes = R.mipmap.icons_padlock,
label = stringResource(R.string.change_password),
modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
onClick = { navController.navigate(NavigationRoute.ChangePasswordScreen.route) }
)
}
// 分割线
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(appColors.divider)
SecurityOptionItem(
iconRes = R.mipmap.icons_block,
label = stringResource(R.string.blocked_users),
onClick = {
// TODO: 导航到屏蔽用户页面
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
NavItem(
iconRes = R.drawable.rider_pro_moment_delete,
SecurityOptionItem(
iconRes = R.mipmap.icons_remove,
label = stringResource(R.string.remove_account),
modifier = Modifier.noRippleClickable {
navController.navigate(NavigationRoute.RemoveAccountScreen.route)
}
onClick = { navController.navigate(NavigationRoute.RemoveAccountScreen.route) },
applyColorFilter = false
)
}
Box(
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.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
@@ -80,6 +81,10 @@ fun MbtiSelectScreen() {
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
navController.navigateUp()
}
)

View File

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

View File

@@ -4,19 +4,26 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
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.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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.Edit as EditIcon
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -28,10 +35,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
@@ -39,26 +50,26 @@ import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
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.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.rememberDebouncedNavigation
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.util.Log
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.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.graphics.SolidColor
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import java.io.File
@@ -94,9 +105,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
val cleanValue = value.replace("\n", "").replace("\r", "")
model.name = cleanValue
usernameError = when {
cleanValue.trim().isEmpty() -> "昵称不能为空"
cleanValue.length < 3 -> "昵称长度不能小于3"
cleanValue.length > 20 -> "昵称长度不能大于20"
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
cleanValue.length > 20 -> context.getString(R.string.error_nickname_too_long)
else -> null
}
}
@@ -108,7 +119,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
val cleanValue = value.replace("\n", "").replace("\r", "")
model.bio = cleanValue
bioError = when {
cleanValue.length > 100 -> "个人简介长度不能大于100"
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
else -> null
}
}
@@ -134,29 +145,313 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
model.reloadProfile()
}
// 设置状态栏为透明,使用浅色图标(因为顶部背景是深色图片)
val systemUiController = rememberSystemUiController()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
}
StatusBarMaskLayout(
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = appColors.background
modifier = Modifier.background(Color(0xFFFAF9FB)),
darkIcons = false, // 浅色图标(白色),因为顶部背景是深色
maskBoxBackgroundColor = Color.Transparent
) {
Column(
Box(
modifier = Modifier
.fillMaxSize()
.background(color = appColors.background),
horizontalAlignment = Alignment.CenterHorizontally
.background(Color(0xFFFAF9FB))
) {
//StatusBarSpacer()
when {
model.isLoading -> {
// 加载中状态
Box(
modifier = Modifier.padding(horizontal = 0.dp, vertical = 16.dp)
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
NoticeScreenHeader(
title = stringResource(R.string.edit_profile),
moreIcon = false
androidx.compose.material3.CircularProgressIndicator(
color = appColors.main
)
}
}
model.profile != null -> {
Column(
modifier = Modifier.fillMaxSize()
) {
Icon(
// 顶部背景区域(圆角在底部,覆盖状态栏)
val banner = model.profile?.banner
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
Box(
modifier = Modifier
.size(24.dp)
.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 = if (AppState.darkMode) {
// TODO: 添加更换封面暗色模式图标
R.mipmap.frame_4 // 临时占位,需替换为实际图标
} else {
// TODO: 添加更换封面亮色模式图标
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, Color(0xFFFAF9FB), 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 = if (AppState.darkMode) {
// TODO: 添加编辑头像暗色模式图标
R.mipmap.frame_4 // 临时占位,需替换为实际图标
} else {
// TODO: 添加编辑头像亮色模式图标
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(Color.White)
) {
// 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(Color(0x41413C43).copy(alpha = 0.2f))
.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 = validate() && !model.isUpdating,
debounceTime = 1000L
@@ -174,177 +469,170 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
}
}
},
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))
contentAlignment = Alignment.Center
) {
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)
text = stringResource(R.string.save),
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.White
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 显示内容或加载状态
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
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 -> {
Log.d("AccountEditScreen2", "显示加载指示器")
// 加载中状态
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
androidx.compose.material3.CircularProgressIndicator(
color = appColors.main
)
}
}
else -> {
Log.d("AccountEditScreen2", "显示错误信息 - 没有数据且不在加载中")
// 没有数据且不在加载中,显示错误信息
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = "加载用户资料失败,请重试",
Text(
text = stringResource(R.string.error_load_profile_failed),
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(Color.White),
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 = Color.Black,
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 = Color(0x993C3C43),
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 = Color.Black
),
cursorBrush = SolidColor(Color.Black),
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
) {
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) {
iconResDark ?: R.mipmap.frame_4 // 使用传入的暗色模式图标,或默认占位
} else {
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
}
),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = iconColor
)
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color.Black
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = value,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0x993C3C43)
)
Icon(
imageVector = Icons.Default.ArrowForward,
contentDescription = null,
modifier = Modifier.size(8.dp),
tint = Color(0x4D3C3C43)
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
package com.aiosman.ravenow.ui.index
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.foundation.ExperimentalFoundationApi
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.Spacer
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -42,8 +54,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.Modifier
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.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
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.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
@@ -123,152 +141,17 @@ fun IndexScreen() {
gesturesEnabled = drawerState.isOpen,
drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Column(
modifier = Modifier
.requiredWidth(250.dp)
.fillMaxHeight()
.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 {
SideMenuContent(
onClose = {
coroutineScope.launch {
drawerState.close()
navController.navigate(NavigationRoute.AccountSetting.route)
}
}
)
Spacer(modifier = Modifier.height(16.dp))
NavItem(
iconRes = R.drawable.rider_pro_favourited,
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)
navController = navController,
context = context,
isDrawerOpen = drawerState.isOpen
)
}
)
// 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)
}
}
)
}
}
}
) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
@@ -447,7 +330,6 @@ fun IndexScreen() {
)
}
}
}
@Composable
@@ -624,3 +506,343 @@ 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) }
// 菜单背景色 #FAF9FB
val menuBackgroundColor = Color(0xFFFAF9FB)
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 白色
val cardBackgroundColor = Color.White
// 跟随系统文字颜色 #979499
val followSystemTextColor = Color(0xFF979499)
// 开关开启颜色 #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(Color.Black)
)
}
// 绝对定位的"扫一扫"文字上方71.5dp右侧66dp
Text(
text = stringResource(R.string.scan_qr),
fontSize = 14.sp,
color = Color.Black,
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(Color.Black)
)
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt绝对定位
Column(
modifier = Modifier
.fillMaxWidth()
.offset(y = 128.dp) // 直接距离顶部128dp整体下移20dp
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 第一组卡片:编辑资料、账号安全、收藏
MenuCard(
backgroundColor = cardBackgroundColor,
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,
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,
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,
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) // 传递compact参数
}
}
}
}
@Composable
fun MenuItemRow(item: MenuItem, compact: Boolean = false) {
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(Color.Black)
)
Text(
text = item.label,
fontSize = 14.sp,
color = Color.Black
)
}
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(Color(0xFF111213))
)
}
}
}

View File

@@ -234,22 +234,24 @@ object AgentViewModel: ViewModel() {
try {
// 获取完整的语言标记(如 "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(
page = 1,
pageSize = 100,
isActive = true,
withChildren = false,
withParent = false,
withCount = true,
hideEmpty = true,
// withChildren = false,
// withParent = false,
// withCount = true,
// hideEmpty = true,
lang = sysLang
)
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 语言标记: $sysLang")
println("分类数据请求完成,响应成功: ${response.isSuccessful}, 原始语言标记: $fullLangTag, 转换后: $sysLang")
if (response.isSuccessful) {
val categoryList = response.body()?.list ?: emptyList()
println("获取到 ${categoryList.size} 个分类")
// 使用当前语言获取翻译后的分类名称
// 使用转换后的语言代码获取翻译后的分类名称
categories = categoryList.map { category ->
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) {
loadAgentData(categoryId)
}

View File

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

View File

@@ -7,6 +7,7 @@ import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.tabs.profile.composable.GalleryGrid
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.SelfProfileAction
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.UserContentPageIndicator
@@ -100,6 +101,13 @@ import kotlinx.coroutines.launch
import java.io.File
import androidx.compose.foundation.rememberScrollState
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)
@Composable
@@ -121,7 +129,8 @@ fun ProfileV3(
postCount: Long? = null, // 新增参数用于传递帖子总数
) {
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 statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
var expanded by remember { mutableStateOf(false) }
@@ -134,7 +143,8 @@ fun ProfileV3(
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val bannerHeight = 400
val bannerWidth = 402
val bannerHeight = 206
val pickBannerImageLauncher = pickupAndCompressLauncher(
context,
scope,
@@ -156,12 +166,13 @@ fun ProfileV3(
val gridState = rememberLazyGridState()
val scrollState = rememberScrollState()
val toolbarAlpha by remember {
// 计算导航栏背景透明度根据滚动位置从0到1
val toolbarBackgroundAlpha by remember {
derivedStateOf {
if (!isSelf) {
1f
} else {
val maxScroll = 500f // 最大滚动距离,可调整
val maxScroll = 600f // 增加最大滚动距离,让渐变更平缓
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
progress
}
@@ -308,12 +319,19 @@ fun ProfileV3(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp)
.background(AppColors.profileBackground)
.background(AppColors.profileBackground),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp - 24.dp)
.width(bannerWidth.dp)
.height(bannerHeight.dp)
.clip(
RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
)
)
.let {
if (isSelf && isMain) {
it.noRippleClickable {
@@ -326,13 +344,6 @@ fun ProfileV3(
it
}
}
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
),
)
) {
CustomAsyncImage(
LocalContext.current,
@@ -347,6 +358,9 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(100.dp))
}
// 壁纸下方间距
Spacer(modifier = Modifier.height(16.dp))
// 用户信息
Box(
modifier = Modifier
@@ -357,31 +371,25 @@ fun ProfileV3(
profile?.let {
UserItem(
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))
// 操作按钮
// 操作按钮(仅其他用户显示)
profile?.let {
if (!isSelf && it.id != AppState.UserId) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
if (isSelf) {
SelfProfileAction(
onEditProfile = {
navController.navigate(NavigationRoute.AccountEdit.route)
},
onPremiumClick = {
navController.navigate(NavigationRoute.VipSelPage.route)
}
)
} else {
if (it.id != AppState.UserId) {
OtherProfileAction(
it,
onFollow = {
@@ -394,7 +402,6 @@ fun ProfileV3(
}
}
}
}
// 用户智能体行
if (!isAiAccount) {
@@ -445,15 +452,14 @@ fun ProfileV3(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier.height(500.dp) // 固定滚动高度
) { idx ->
when (idx) {
0 ->
GalleryGrid(moments = moments)
1 ->
0 -> GalleryGrid(moments = moments)
1 -> {
if (!isAiAccount) {
UserAgentsList(
agents = agents,
onAgentClick = onAgentClick,
@@ -475,10 +481,22 @@ fun ProfileV3(
},
modifier = Modifier.fillMaxSize()
)
} else {
GroupChatPlaceholder()
}
}
2 -> {
if (!isAiAccount) {
GroupChatPlaceholder()
}
}
}
}
}
// 底部间距,增加滚动距离
Spacer(modifier = Modifier.height(100.dp))
}
// 顶部导航栏
TopNavigationBar(
@@ -486,9 +504,13 @@ fun ProfileV3(
isSelf = isSelf,
profile = profile,
navController = navController,
alpha = toolbarAlpha,
backgroundAlpha = toolbarBackgroundAlpha,
interactionCount = moments.sumOf { it.likeCount }, // 计算总点赞数作为互动数据
onMenuClick = {
showOtherUserMenu = true
},
onShareClick = {
// TODO: 实现分享功能
}
)
@@ -562,6 +584,11 @@ fun ProfileV3(
}
}
@Composable
private fun GroupChatPlaceholder() {
GroupChatEmptyContent()
}
//顶部导航栏组件
@Composable
fun TopNavigationBar(
@@ -569,30 +596,161 @@ fun TopNavigationBar(
isSelf: Boolean,
profile: AccountProfileEntity?,
navController: androidx.navigation.NavController,
alpha: Float,
onMenuClick: () -> Unit = {}
backgroundAlpha: Float,
interactionCount: Int = 0,
onMenuClick: () -> Unit = {},
onShareClick: () -> Unit = {}
) {
val appColors = LocalAppTheme.current
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
// 根据背景透明度决定图标颜色透明度为1时变黑否则为白色
val iconColor = if (backgroundAlpha >= 1f) Color.Black else Color.White
val cardBorderColor = if (backgroundAlpha >= 1f) Color.Black else Color.White
Box(
modifier = Modifier
.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)
// 初始状态:半透明深色,让白色图标清晰可见
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
.fillMaxWidth()
.background(appColors.profileBackground)
) {
StatusBarSpacer()
.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(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
},
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp) // 增加上下内边距
.align(Alignment.TopEnd)
.padding(top = statusBarHeight), // 从状态栏下方开始
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
// 左侧:互动数据卡片
Row(
modifier = Modifier
.height(24.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 = 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 {
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()
Row(
modifier = Modifier
.align(Alignment.TopStart)
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = statusBarPadding.calculateTopPadding()),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
@@ -601,7 +759,7 @@ fun TopNavigationBar(
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
colorFilter = ColorFilter.tint(Color.White)
)
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
@@ -618,55 +776,81 @@ fun TopNavigationBar(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = appColors.text
)
}
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)
color = Color.White
)
}
}
}
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
}
// 分享图标(向上箭头)
@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
)
}
}

View File

@@ -199,7 +199,7 @@ fun GalleryGrid(
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = "你的故事还没开始",
text = stringResource(R.string.your_story_not_started),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
@@ -208,7 +208,7 @@ fun GalleryGrid(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "发布一条动态,和世界打个招呼吧",
text = stringResource(R.string.publish_moment_greeting),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400

View File

@@ -0,0 +1,168 @@
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.Divider
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.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.R
@Composable
fun GroupChatEmptyContent() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
// 分割线(紧贴上方栏)
Divider(
color = Color(0xFFF0F0F0), // 更浅的灰色
thickness = 1.dp,
modifier = Modifier.fillMaxWidth()
)
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 = Color(0xFF000000)
)
}
}
}
@Composable
private fun SegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
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
)
Spacer(modifier = Modifier.width(8.dp))
// 公开
SegmentButton(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp
)
Spacer(modifier = Modifier.width(8.dp))
// 私有
SegmentButton(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp
)
}
}
@Composable
private fun SegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp
) {
Box(
modifier = Modifier
.width(width)
.height(32.dp)
.background(
color = if (isSelected) {
Color(0xFF110C13) // RGB(17, 12, 19)
} 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) Color(0xFFFFFFFF) else Color(0xFF000000)
)
}
}
@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 = 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,
fontWeight = FontWeight.W900,
color = if (profile.isFollowing) {

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -60,15 +61,14 @@ fun UserAgentsList(
) {
val AppColors = LocalAppTheme.current
if (agents.isEmpty()) {
// 使用带分段控制器的空状态布局
AgentEmptyContentWithSegments()
} else {
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (agents.isEmpty()) {
item {
EmptyAgentsView()
}
} else {
items(agents) { agent ->
UserAgentCard(
agent = agent,
@@ -76,13 +76,13 @@ fun UserAgentsList(
onAvatarClick = onAvatarClick
)
}
}
// 底部间距
item {
Spacer(modifier = Modifier.height(120.dp))
}
}
}
}
@Composable
@@ -198,6 +198,178 @@ 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)
) {
// 分割线(紧贴上方栏)
Divider(
color = Color(0xFFF0F0F0), // 更浅的灰色
thickness = 1.dp,
modifier = Modifier.fillMaxWidth()
)
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
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.ai_companion_not_tool),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
} 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
) {
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
)
Spacer(modifier = Modifier.width(8.dp))
// 公开
AgentSegmentButton(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp
)
Spacer(modifier = Modifier.width(8.dp))
// 私有
AgentSegmentButton(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp
)
}
}
@Composable
private fun AgentSegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp
) {
Box(
modifier = Modifier
.width(width)
.height(32.dp)
.background(
color = if (isSelected) {
Color(0xFF110C13) // RGB(17, 12, 19)
} 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) Color(0xFFFFFFFF) else Color(0xFF000000)
)
}
}
@Composable
fun EmptyAgentsView() {
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -52,7 +45,7 @@ fun UserContentPageIndicator(
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 图片/相册 Tab
// 动态 Tab
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
@@ -64,15 +57,24 @@ fun UserContentPageIndicator(
}
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_images),
contentDescription = "Gallery",
tint = if (pagerState.currentPage == 0) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
Text(
text = stringResource(R.string.index_dynamic),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 0) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 0) Color(0xFF000000) else Color(0x993C3C43)
)
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(Color(0xFF000000))
)
}
}
// Agent Tab (只在非智能体用户时显示)
// 智能体 Tab (只在非智能体用户时显示)
if (showAgentTab) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -85,40 +87,54 @@ fun UserContentPageIndicator(
}
.padding(vertical = 12.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_nav_ai),
contentDescription = "Agents",
tint = if (pagerState.currentPage == 1) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.size(24.dp)
Text(
text = stringResource(R.string.chat_ai),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 1) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 1) Color(0xFF000000) else Color(0x993C3C43)
)
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(Color(0xFF000000))
)
}
}
}
// 下划线指示器
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.height(2.dp)
.background(
if (pagerState.currentPage == 0) AppColors.text else Color.Transparent
)
)
// 群聊 Tab (只在非智能体用户时显示)
if (showAgentTab) {
Box(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.noRippleClickable {
scope.launch {
pagerState.scrollToPage(2)
}
}
.padding(vertical = 12.dp)
) {
Text(
text = stringResource(R.string.chat_group),
fontSize = 16.sp,
fontWeight = if (pagerState.currentPage == 2) FontWeight.SemiBold else FontWeight.Medium,
color = if (pagerState.currentPage == 2) Color(0xFF000000) else Color(0x993C3C43)
)
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(
if (pagerState.currentPage == 1) AppColors.text else Color.Transparent
)
.background(Color(0xFF000000))
)
}
}
}
}
}
}

View File

@@ -1,26 +1,39 @@
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.fillMaxWidth
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.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
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.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import java.text.NumberFormat
import java.util.Locale
@Composable
fun UserItem(
accountProfileEntity: AccountProfileEntity,
postCount: Long = 0
postCount: Long = 0,
isSelf: Boolean = false,
onEditClick: () -> Unit = {}
) {
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
val followerDebouncer = 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(
modifier = Modifier
.fillMaxWidth()
) {
// 顶部:头像和统计数据
Row(
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 头像
CustomAsyncImage(
@@ -53,39 +95,47 @@ fun UserItem(
accountProfileEntity.avatar,
modifier = Modifier
.clip(CircleShape)
.size(48.dp),
.size(96.dp),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(32.dp))
//个人统计
// 统计数据
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(0.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 帖子数
Column(
modifier = Modifier
.width(80.dp)
.height(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f)
verticalArrangement = Arrangement.Center
) {
Text(
text = postCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp,
color = AppColors.text
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "帖子",
color = AppColors.text
text = stringResource(R.string.posts),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
}
// 粉丝数
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.width(80.dp)
.height(40.dp)
.noRippleClickable {
followerDebouncer {
navController.navigate(
@@ -95,26 +145,33 @@ fun UserItem(
)
)
}
}
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = accountProfileEntity.followerCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp,
color = AppColors.text
text = formattedFollowerCount,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "粉丝",
color = AppColors.text
text = stringResource(R.string.followers_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
}
// 关注数
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.width(80.dp)
.height(40.dp)
.offset(x = 6.dp)
.noRippleClickable {
followingDebouncer {
navController.navigate(
@@ -124,49 +181,161 @@ fun UserItem(
)
)
}
}
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp,
color = AppColors.text
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "关注",
color = AppColors.text
text = stringResource(R.string.following_upper),
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = Color(0xFF000000),
textAlign = TextAlign.Center
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// 昵称
Text(
text = accountProfileEntity.nickName,
fontWeight = FontWeight.W600,
fontSize = 16.sp,
color = AppColors.text
)
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()){
Text(
text = accountProfileEntity.bio,
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}else{
Text(
text = "No bio here.",
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// 中间:昵称、简介、创建者信息
Column(
modifier = Modifier
.fillMaxWidth()
) {
// 昵称
Text(
text = accountProfileEntity.nickName,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
letterSpacing = (-0.3).sp,
color = Color(0xFF000000)
)
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()) {
Text(
text = accountProfileEntity.bio,
fontSize = 13.sp,
color = Color(0x993C3C43), // 60/255, 60/255, 67/255, alpha 0.6
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
Text(
text = "Welcome to my fantiac word i will show you something about magic",
fontSize = 13.sp,
color = Color(0x993C3C43),
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 = Color(0xFF000000)
)
}
// 星座标签
if (!zodiac.isNullOrEmpty()) {
ProfileTag(
text = zodiac,
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
textColor = Color(0xFF000000)
)
}
// 编辑标签(仅自己可见)
if (isSelf) {
ProfileTag(
text = stringResource(R.string.edit_profile),
backgroundColor = Color(0x14947A80), // 124/255, 116/255, 128/255, alpha 0.08
textColor = Color(0xFF9284BD), // 146/255, 132/255, 189/255
leadingIcon = {
EditIcon(
color = Color(0xFF9284BD),
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,
fontSize = 12.sp,
color = textColor
)
}
}
}
@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

@@ -1,15 +1,19 @@
package com.aiosman.ravenow.ui.login
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -19,9 +23,13 @@ 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.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
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.api.getErrorMessageCode
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.CheckboxWithLabel
import com.aiosman.ravenow.ui.composables.PolicyCheckbox
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TextInputField
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.PasswordValidator
import kotlinx.coroutines.Dispatchers
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
fun EmailSignupScreen() {
var appColor = LocalAppTheme.current
val appColor = LocalAppTheme.current
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) }
var acceptTerms by remember { mutableStateOf(false) }
var acceptPromotions by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val context = LocalContext.current
@@ -60,14 +71,13 @@ fun EmailSignupScreen() {
var passwordError by remember { mutableStateOf<String?>(null) }
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var termsError by remember { mutableStateOf<Boolean>(false) }
var promotionsError by remember { mutableStateOf<Boolean>(false) }
fun validateForm(): Boolean {
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)
.matches() -> context.getString(R.string.text_error_email_format_1)
else -> null
}
@@ -88,22 +98,8 @@ fun EmailSignupScreen() {
}
termsError = true
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
}
return emailError == null && passwordError == null && confirmPasswordError == null
}
@@ -158,63 +154,127 @@ fun EmailSignupScreen() {
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(appColor.background)
) {
StatusBarSpacer()
Box(
// 顶部导航栏:返回箭头 + "注册"标题,左对齐
Row(
modifier = Modifier
.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(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 24.dp)
.padding(horizontal = 0.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// 邮箱输入框
TextInputField(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = email,
onValueChange = {
email = it
},
hint = stringResource(R.string.text_hint_email),
error = emailError
label = stringResource(R.string.login_email_label),
hint = "输入电子邮件",
error = emailError,
leadingIcon = {
Image(
painter = painterResource(id = R.mipmap.icon_email_light),
contentDescription = "Email",
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(IconGray)
)
Spacer(modifier = Modifier.padding(4.dp))
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
)
// 密码输入框
TextInputField(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = password,
onValueChange = {
password = it
},
password = true,
label = stringResource(R.string.text_hint_password).replace("输入", ""),
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)
)
Spacer(modifier = Modifier.padding(4.dp))
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
)
// 确认密码输入框
TextInputField(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.padding(horizontal = 24.dp),
text = confirmPassword,
onValueChange = {
confirmPassword = it
},
password = true,
hint = stringResource(R.string.text_hint_confirm_password),
error = confirmPasswordError
label = stringResource(R.string.confirm_password_label),
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)
)
Spacer(modifier = Modifier.height(8.dp))
},
customBackgroundColor = LightGrayBackground,
customCornerRadius = 16f
)
Spacer(modifier = Modifier.height(16.dp))
// 功能选项区域
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.Start,
) {
CheckboxWithLabel(
@@ -236,42 +296,31 @@ fun EmailSignupScreen() {
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(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
contentAlignment = Alignment.Center
) {
ActionButton(
modifier = Modifier
.width(345.dp),
text = stringResource(R.string.lets_ride_upper),
backgroundColor = Color(0xffda3832),
color = Color.White
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_up_upper),
backgroundColor = PurpleButton,
color = Color.White,
fontSize = 17.sp,
fontWeight = FontWeight.W600
) {
scope.launch(Dispatchers.IO) {
registerUser()
}
}
}
}
}
}

View File

@@ -72,8 +72,17 @@ fun UserAuthScreen() {
var passwordError by remember { mutableStateOf<String?>(null) }
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
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)

View File

@@ -52,6 +52,8 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
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.ColorFilter
import androidx.compose.ui.graphics.PathEffect
@@ -151,71 +153,42 @@ fun NewPostScreen() {
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.padding(horizontal = 16.dp)
.background(AppColors.divider)
)
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
.padding(start = 16.dp)
.width(100.dp)
.height(40.dp)
.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(
painter = painterResource(id = R.mipmap.rider_pro_moment_ai),
painter = painterResource(id = R.mipmap.icon_ai),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.size(16.dp)
)
Text(
text = stringResource(R.string.moment_ai_co),
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
fontWeight = FontWeight.Normal,
fontSize = 13.sp,
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
color = AppColors.text,
)
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)
.padding(start = 2.dp),
color = Color.White,
)
}
@@ -352,7 +325,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
modifier = Modifier.align(Alignment.CenterStart),
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_close),
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.size(24.dp)
@@ -366,9 +339,31 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
},
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))
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",
modifier = Modifier
.size(24.dp)
@@ -391,11 +386,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
}finally {
uploading = false
}
}
}
},
)
}
@@ -488,72 +480,24 @@ fun AddImageGrid() {
}
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
LazyVerticalGrid(
columns = GridCells.Fixed(5),
contentPadding = PaddingValues(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
contentPadding = PaddingValues(horizontal = 19.dp, vertical = 4.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 添加按钮
if (canAddMoreImages) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)) // 设置圆角
.background(AppColors.basicMain) // 设置背景色
.clip(RoundedCornerShape(24.dp))
.background(Color(0xFFFAF9FB))
.noRippleClickable {
addImageDebouncer {
if (model.imageList.size < 9) {
@@ -572,20 +516,21 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = "Add Image",
modifier = Modifier
.size(24.dp)
.size(23.3.dp)
.align(Alignment.Center),
tint = AppColors.nonActiveText
)
}
}
// 相机按钮
item {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)) // 设置圆角
.background(AppColors.basicMain) // 设置背景色
.clip(RoundedCornerShape(24.dp))
.background(Color(0xFFFAF9FB))
.noRippleClickable {
if (model.imageList.size < 9) {
val photoFile = File(context.cacheDir, "photo.jpg")
@@ -605,13 +550,60 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "Take Photo",
modifier = Modifier
.size(24.dp)
.size(23.3.dp)
.align(Alignment.Center),
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.MomentEntity
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.FollowChangeEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent
@@ -166,7 +167,8 @@ class PostViewModel(
moment?.let {
userService.followUser(it.authorId.toString())
moment = moment?.copy(followStatus = true)
// 更新我的关注页面的关注数
// 发送关注事件,通知动态列表更新关注状态
EventBus.getDefault().post(FollowChangeEvent(it.authorId, true))
}
}
@@ -174,7 +176,8 @@ class PostViewModel(
moment?.let {
userService.unFollowUser(it.authorId.toString())
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

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