109 Commits

Author SHA1 Message Date
234587afc9 缁х画瀹屽杽鍔熻兘鏇存柊 2025-11-10 20:30:31 +08:00
b855acd8d3 Merge remote-tracking branch 'origin/main' into feat/pr-20251104-154907-clean 2025-11-10 19:48:35 +08:00
391f841f45 Merge pull request #59 from Kevinlinpr/atm2
feat: 新增短视频功能
2025-11-10 19:47:34 +08:00
2ed5639cbc feat: 新增短视频功能
- 新增短视频信息流页面,支持上下滑动切换视频。
- 实现视频播放、暂停、加载、空状态及错误处理等基础功能。
- 在视频页面中集成点赞、评论、收藏等互动操作。
- 后端接口新增 `videoFilter` 参数,用于仅获取包含视频的动态。
- 扩展了 `MomentEntity` 和相关数据模型,以支持视频数据。
- 将短视频页面集成到动态(Moment)Tab中。
2025-11-10 17:55:33 +08:00
2cbd2a975f Reapply "增加英文日语翻译 修改编辑资料界面无法更改星座mbit"
This reverts commit a057f7f7fd.
2025-11-10 15:26:31 +08:00
6590b09300 Merge pull request #55 from Kevinlinpr/zhong_1
修改BUG:登录账号邮箱格式错误无提示
2025-11-10 14:06:17 +08:00
30563a75f3 Merge branch 'main' into zhong_1 2025-11-10 11:20:08 +08:00
aac3220a69 Merge pull request #56 from Kevinlinpr/feat/pr-20251104-154907
我的(侧边栏)ui调整
2025-11-10 11:05:12 +08:00
e2de134180 Merge branch 'main' into zhong_1
Merge main branch to keep zhong_1 up to date# Please enter a commit message to explain why this merge is necessary,
2025-11-10 10:19:44 +08:00
7f1be94896 Merge pull request #54 from Kevinlinpr/atm2
优化分类数据加载的语言参数
2025-11-10 10:07:35 +08:00
1c048fd9c0 发布动态页面UI调整 2025-11-07 21:30:56 +08:00
2613d2e801 修复bug:关注用户或者AI后关注列表能正常显示 2025-11-07 21:29:03 +08:00
c100a8ceef 注册账号界面ui调整 2025-11-07 21:25:02 +08:00
f86b5e1d39 账号安全界面ui设置 2025-11-07 21:22:10 +08:00
75eb38b188 我的界面ui设置,新加群聊标签以及缺省图 2025-11-07 21:19:17 +08:00
4f588483c0 Merge branch 'feat/pr-20251104-154907' of https://github.com/Kevinlinpr/rider-pro-android-app into feat/pr-20251104-154907 2025-11-07 14:42:17 +08:00
0bc442762d 我的-页面顶部导航栏ui修改,增加下滑时顶部导航栏的变化效果以及壁纸头像大小修正 2025-11-07 14:36:25 +08:00
397ac6a9ee Merge branch 'main' into feat/pr-20251104-154907 2025-11-07 13:56:53 +08:00
784f87dc39 recover 2025-11-07 10:46:08 +08:00
e714f567b9 我的-编辑-ui调整 2025-11-06 21:34:08 +08:00
703beb8d43 我的(侧边栏)ui调整 2025-11-06 21:23:51 +08:00
2b30beb367 资源文件替换 2025-11-06 20:56:20 +08:00
6fffa0447e 修复文件将所有 PromptRule 引用改为 AgentRule,并更新相关字段访问 2025-11-06 20:49:10 +08:00
2a9d6a2f6b 抓包配置文件 2025-11-06 18:15:00 +08:00
cc12a08472 修改BUG:登录账号邮箱格式错误无提示 2025-11-06 10:53:34 +08:00
513897499d 优化分类数据加载的语言参数
根据系统语言标签(如 "zh-CN")将其转换为后端支持的语言代码(zh, cn, ja),用于请求分类数据。默认回退到 "zh"。
2025-11-06 10:20:28 +08:00
baa6f284bd Merge pull request #53 from Kevinlinpr/atm
新增智能体和房间规则管理功能
2025-11-06 10:09:16 +08:00
2ba4c2ec02 Merge branch 'main' into atm 2025-11-05 23:15:50 +08:00
28061617da Merge pull request #49 from Kevinlinpr/zhong_1
记忆管理功能实现
2025-11-05 22:30:50 +08:00
c9e411c4ad Merge branch 'main' into zhong_1 2025-11-05 22:30:29 +08:00
995e061b6f Merge pull request #52 from Kevinlinpr/feat/pr-20251104-154907
添加深色模式缺省图
2025-11-05 22:25:09 +08:00
9c9eb66b71 新增智能体和房间规则管理功能
- 新增智能体(Agent)规则的增删改查(CRUD)和服务实现。
- 新增房间(Room)规则的增删改查(CRUD)和服务实现。
- 将 `PromptRule` 相关命名重构为 `AgentRule`,以提高代码清晰度。
- 将相关数据实体中表示总数的字段类型从 `Int` 修改为 `Long`。
2025-11-05 22:24:03 +08:00
f90bfbfa0f 添加深色模式缺省图 2025-11-05 21:31:11 +08:00
9ea03cee34 群记忆项编辑功能;群权限设置UI;群记忆管理缺省图 2025-11-05 21:29:18 +08:00
1de8bb825c Merge pull request #51 from Kevinlinpr/revert-48-feat/pr-20251104-154907
Revert "Feat/pr 20251104 154907"
2025-11-05 16:57:59 +08:00
721b7aa9ab Revert "Feat/pr 20251104 154907" 2025-11-05 16:49:17 +08:00
f16be90cc3 Merge pull request #50 from Kevinlinpr/feat/pr-20251104-154907
修改底部导航栏颜色,暗夜模式缺省图
2025-11-05 16:44:23 +08:00
4a1c15747c 文本资源文件替换 2025-11-04 19:01:11 +08:00
0bbfd9b739 修改底部导航栏颜色,暗夜模式缺省图 2025-11-04 18:59:18 +08:00
cff6b78c30 记忆管理功能实现 2025-11-04 18:28:24 +08:00
13593212df Merge pull request #48 from Kevinlinpr/feat/pr-20251104-154907
Feat/pr 20251104 154907
2025-11-04 16:01:05 +08:00
56f225702e Merge origin/main into feat/pr-20251104-154907 (prefer main); resolve gradlew via theirs 2025-11-04 15:58:50 +08:00
0a5b81cc5c test 2025-11-04 14:55:09 +08:00
10fc40373d test 2025-11-04 14:50:20 +08:00
f18ee9e360 test 2025-11-04 14:40:01 +08:00
9262288bc9 Merge pull request #47 from Kevinlinpr/atm
新增智能体规则相关数据类及API接口,包括创建、修改、删除规则功能和查询规则列表及配额信息
2025-11-04 11:01:25 +08:00
3273b17d15 新增智能体规则相关数据类及API接口,包括创建、修改、删除规则功能和查询规则列表及配额信息 2025-11-04 10:59:59 +08:00
4ef5a94d46 Merge pull request #46 from Kevinlinpr/zhong_1
实现新闻界面查看全文
2025-11-03 10:10:28 +08:00
4a684886fa 聊天自定义背景实现 2025-10-31 16:41:39 +08:00
d7f87c7c55 新增聊天设置页面;自定义背景UI 2025-10-31 11:57:49 +08:00
00933dadb8 实现新闻界面查看全文 2025-10-29 18:08:24 +08:00
d5cc186e27 Merge pull request #45 from Kevinlinpr/zhong_1
登录界面UI调整;新增新闻评论
2025-10-28 18:45:22 +08:00
90156745ad 添加新闻接口,UI调整 2025-10-28 18:42:05 +08:00
7095832722 登录界面UI调整;新增新闻评论 2025-10-27 18:51:42 +08:00
658b337d22 Merge pull request #44 from Kevinlinpr/zhong_1
动态、热门界面UI调整
2025-10-27 11:16:47 +08:00
f6a760371a 新增新闻标签页;修改底部导航栏背景颜色 2025-10-24 18:27:58 +08:00
13ed16078b 新增苹果账户登录;新增拉黑功能;UI调整 2025-10-23 17:56:24 +08:00
2a5174cbb6 动态、热门界面UI调整 2025-10-22 18:52:46 +08:00
ea26cb40a0 Merge pull request #43 from Kevinlinpr/zhong_1
热门聊天室实现;首页UI调整
2025-10-21 21:39:20 +08:00
eb8119b775 动态页面顶部标签 2025-10-21 18:35:22 +08:00
66da741eda 登录页面UI调整 2025-10-21 17:19:32 +08:00
18dd52e193 聊天室显示个数 2025-10-20 17:05:48 +08:00
f839a793a3 热门聊天室实现;首页UI调整 2025-10-20 16:54:23 +08:00
54df6c088b Merge pull request #41 from Kevinlinpr/home_page
主页推荐agent实现
2025-10-17 17:23:28 +08:00
28fb94a824 顶部 推荐 agent 列表实现 2025-10-17 17:21:52 +08:00
4bdbbb0231 主页分类动态获取 2025-10-17 16:56:25 +08:00
58a2013a8f Merge pull request #40 from Kevinlinpr/lottie_deps
lottie 依赖
2025-10-17 16:11:15 +08:00
2f2da0a159 lottie 依赖 2025-10-17 15:27:12 +08:00
4ffaf3c3a8 Merge pull request #38 from Kevinlinpr/zhong
编辑资料页面UI调整:添加横幅图片区域
2025-10-16 18:10:11 +08:00
29490d288b “我的”页面UI调整 2025-10-16 18:06:24 +08:00
a99ab30c4e 编辑资料页面UI调整:添加横幅图片区域 2025-10-15 18:54:04 +08:00
0442925ae9 Merge pull request #37 from Kevinlinpr/zhong
解决问题:首页智能体头像为默认头像时显示为空
2025-10-14 18:20:22 +08:00
9d3d13a22d 新增消息界面“全部”标签页 2025-10-14 17:40:25 +08:00
f0a9704e2d 个人信息页和用户信息页UI调整 2025-10-13 18:49:47 +08:00
7f2c103ada 修改首页智能体头像显示逻辑,先显示默认头像(所有情况都显示)如果有网络头像则覆盖显示。 2025-10-11 18:55:39 +08:00
bd01ae39d0 Merge pull request #36 from Kevinlinpr/zhong
UI调整
2025-10-10 21:42:24 +08:00
d94e3b5c20 消息界面通知按钮点击事件;新增通知界面 2025-10-10 18:41:57 +08:00
44cc76d2e3 日文资源文件
实现重新加载功能
收藏界面UI调整
2025-10-09 17:36:06 +08:00
fac6f23356 新建文件夹 app/src/main/assets 将.lottie文件放入
UI调整
2025-09-30 18:54:35 +08:00
39928abc46 Merge pull request #35 from Zhong202501/main
缺省图
2025-09-30 15:53:37 +08:00
4d0d7004b0 缺省图 2025-09-29 18:29:59 +08:00
28a6e3fef3 Merge pull request #34 from Zhong202501/main
添加启动界面
2025-09-28 18:50:51 +08:00
b275d88ef7 添加启动界面 2025-09-28 18:24:54 +08:00
595ef7f942 Merge pull request #33 from Zhong202501/main
Agent创建成功全局显示; 适配暗黑模式
2025-09-27 20:11:44 +08:00
1202b55c74 text颜色 2025-09-26 18:50:51 +08:00
074009d256 添加界面状态恢复逻辑 2025-09-26 18:31:27 +08:00
ad1de9e3f7 Agent创建成功全局显示;
适配暗黑模式
2025-09-26 17:01:46 +08:00
359bcfdfd7 Agent创建成功全局显示;
适配暗黑模式
2025-09-26 17:00:12 +08:00
1901fddb2e Merge pull request #32 from Zhong202501/main
创建弹窗UI调整
2025-09-26 11:29:03 +08:00
b96ae94bdb 界面逻辑优化 2025-09-25 18:32:34 +08:00
dedd356896 创建弹窗UI调整 2025-09-24 18:51:20 +08:00
bb9fda75ae Merge pull request #31 from Zhong202501/main
AI美化功能; 输入框逻辑优化; 文本资源文件;
2025-09-24 14:35:08 +08:00
ea911f113b AI美化功能 2025-09-23 18:32:01 +08:00
88f379fe5b Merge pull request #30 from Kevinlinpr/agent_scroll
优化AI界面,添加分页加载功能,支持动态加载更多智能体数据;重构UI布局
2025-09-23 13:43:55 +08:00
1a24136c35 优化AI界面,添加分页加载功能,支持动态加载更多智能体数据;重构UI布局 2025-09-23 11:57:11 +08:00
742410223c Merge pull request #29 from Zhong202501/main
手动创建AI界面调整
2025-09-23 10:37:15 +08:00
bd5aff7564 手动创建AI界面调整 2025-09-22 17:57:39 +08:00
b43c1585c4 Merge pull request #28 from Zhong202501/main
创建AI界面UI兼容;动态页面调整
2025-09-22 10:48:57 +08:00
cb582393f1 手动创造AI界面;调整输入框点击区域;创建AI时的三点彩色动画 2025-09-19 18:45:10 +08:00
a200d00587 UI调整 2025-09-18 18:19:19 +08:00
6d2133545f 动态详情页面评论调整 2025-09-18 18:16:54 +08:00
2aad126010 创建AI界面UI兼容;动态页面调整 2025-09-17 18:41:15 +08:00
b7b777d2d0 Merge pull request #26 from Zhong202501/new
首页底部导航栏图标;创建AI界面
2025-09-17 11:03:17 +08:00
e804c8be0c Merge pull request #20 from Kevinlinpr/new-bottom-create-button
Feat: Add Create Bottom Sheet and icons
2025-09-17 10:44:34 +08:00
228a74695e Merge pull request #22 from Zhong202501/main
添加Category接口
2025-09-17 10:39:22 +08:00
41a51b85da 首页底部导航栏图标;创建AI界面 2025-09-16 18:18:36 +08:00
e74e8615a5 Agent卡片组件UI;Agent聊天界面输入框显示问题 2025-09-15 14:06:05 +08:00
349d39daf2 修复BUG:我的界面右上角图标会跟随背景图一起向上滑走 2025-09-12 18:24:49 +08:00
8154a0ddc4 Category接口;Agent卡片组件背景颜色 2025-09-11 18:14:54 +08:00
488 changed files with 13802 additions and 2845 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-09T09:51:06.656104400Z"> <DropdownSelection timestamp="2025-11-05T12:24:27.034893100Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.227:45035;connection=094cb92e" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=c328a150" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

32
.idea/gradle.xml generated
View File

@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option> </option>
<option name="resolveExternalAnnotations" value="false" /> </component>
</GradleProjectSettings>
</option>
</component>
</project> </project>

View File

@@ -125,7 +125,7 @@ dependencies {
// 添加 lifecycle-runtime-ktx 依赖 // 添加 lifecycle-runtime-ktx 依赖
implementation(libs.androidx.lifecycle.runtime.ktx.v262) implementation(libs.androidx.lifecycle.runtime.ktx.v262)
implementation (libs.eventbus) implementation (libs.eventbus)
implementation(libs.lottie)
} }

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -45,6 +45,8 @@ object AppState {
var googleClientId: String? = null var googleClientId: String? = null
var enableGoogleLogin: Boolean = false var enableGoogleLogin: Boolean = false
var enableChat = false var enableChat = false
var agentCreatedSuccess by mutableStateOf(false)
var chatBackgroundUrl by mutableStateOf<String?>(null)
suspend fun initWithAccount(scope: CoroutineScope, context: Context) { suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
// 如果是游客模式,使用简化的初始化流程 // 如果是游客模式,使用简化的初始化流程
if (AppStore.isGuest) { if (AppStore.isGuest) {

View File

@@ -5,7 +5,8 @@ object ConstVars {
// Debug: http://192.168.0.201:8088 // Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api // Release: https://rider-pro.aiosman.com/beta_api
val BASE_SERVER = if (BuildConfig.DEBUG) { val BASE_SERVER = if (BuildConfig.DEBUG) {
"http://47.109.137.67:6363" // Debug环境 // "http://47.109.137.67:6363" // Debug环境
"https://rider-pro.aiosman.com/beta_api" // Release环境
} else { } else {
"https://rider-pro.aiosman.com/beta_api" // Release环境 "https://rider-pro.aiosman.com/beta_api" // Release环境
} }

View File

@@ -43,7 +43,12 @@ import com.google.firebase.analytics.analytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.aiosman.ravenow.ui.splash.SplashScreen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// Firebase Analytics // Firebase Analytics
@@ -79,10 +84,10 @@ class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.P) @RequiresApi(Build.VERSION_CODES.P)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 设置屏幕方向为竖屏 // 设置屏幕方向为竖屏
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// 监听应用生命周期 // 监听应用生命周期
ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver()) ProcessLifecycleOwner.get().lifecycle.addObserver(MainActivityLifecycleObserver())
// 创建通知渠道 // 创建通知渠道
@@ -122,75 +127,84 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
CompositionLocalProvider( var showSplash by remember { mutableStateOf(true) }
LocalAppTheme provides AppState.appTheme
) { LaunchedEffect(Unit) {
CheckUpdateDialog() kotlinx.coroutines.delay(2000)
Navigation(startDestination) { navController -> showSplash = false
// 处理带有 postId 的通知点击 }
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID") if (showSplash) {
val action = intent.getStringExtra("ACTION") SplashScreen()
if (action == "newFollow") { } else {
navController.navigate(NavigationRoute.Followers.route) CompositionLocalProvider(
return@Navigation LocalAppTheme provides AppState.appTheme
} ) {
if (action == "followCount") { CheckUpdateDialog()
navController.navigate(NavigationRoute.Followers.route) Navigation(startDestination) { navController ->
return@Navigation
} // 处理带有 postId 的通知点击
if (action == "TRTC_NEW_MESSAGE") { val postId = intent.getStringExtra("POST_ID")
val userService:UserService = UserServiceImpl() var commentId = intent.getStringExtra("COMMENT_ID")
val sender = intent.getStringExtra("SENDER") val action = intent.getStringExtra("ACTION")
sender?.let { if (action == "newFollow") {
scope.launch { navController.navigate(NavigationRoute.Followers.route)
try { return@Navigation
val profile = userService.getUserProfileByTrtcUserId(it,0) }
navController.navigate(NavigationRoute.Chat.route.replace( if (action == "followCount") {
"{id}", navController.navigate(NavigationRoute.Followers.route)
profile.id.toString() return@Navigation
)) }
}catch (e:Exception){ if (action == "TRTC_NEW_MESSAGE") {
e.printStackTrace() val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it,0)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
}
} }
} }
return@Navigation
} }
return@Navigation
}
if (commentId == null) { if (commentId == null) {
commentId = "0" commentId = "0"
}
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
} }
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
}
} }
} }
} }
} }
} }
} }
/** /**
* 请求通知权限 * 请求通知权限
*/ */

View File

@@ -2,8 +2,16 @@ package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AgentRule
import com.aiosman.ravenow.data.api.AgentRuleListResponse
import com.aiosman.ravenow.data.api.AgentRuleQuota
import com.aiosman.ravenow.data.api.AgentRuleAgent
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
import com.aiosman.ravenow.data.api.InsufficientBalanceError
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.ProfileEntity import com.aiosman.ravenow.entity.ProfileEntity
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class Agent( data class Agent(
@@ -83,9 +91,16 @@ data class Profile(
) )
} }
} }
/**
* 智能体服务
*/
interface AgentService { interface AgentService {
/** /**
* 获取智能体列表 * 获取智能体列表
* @param pageNumber 页码
* @param pageSize 每页数量,默认 20
* @param authorId 作者ID可选参数用于筛选特定作者的智能体
* @return 智能体列表容器,包含分页信息和智能体列表,失败时返回 null
*/ */
suspend fun getAgent( suspend fun getAgent(
pageNumber: Int, pageNumber: Int,
@@ -96,3 +111,206 @@ interface AgentService {
} }
// ========== Agent 规则 - 领域实体 ==========
data class AgentRuleAgentInfo(
val id: Int,
val title: String,
val avatar: String,
)
data class AgentRuleEntity(
val id: Int,
val rule: String,
val creator: String,
val creatorType: String,
val scope: String,
val agent: AgentRuleAgentInfo,
val createdAt: String,
val updatedAt: String,
)
data class AgentRuleListResult(
val page: Int,
val pageSize: Int,
val total: Int,
val list: List<AgentRuleEntity>,
)
data class AgentRuleQuotaEntity(
val agentId: Int,
val agentTitle: String,
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double,
)
// ========== Agent 规则 - Service 接口 ==========
/**
* Agent 规则服务
*/
interface AgentRuleService {
/**
* 根据 OpenId 创建 Agent 规则
* @param openId Agent 的 OpenId
* @param rule 规则内容,不能为空
* @throws ServiceException 创建失败时抛出异常(包括余额不足等情况)
*/
suspend fun createAgentRuleByOpenId(openId: String, rule: String)
/**
* 修改 Agent 规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @param openId Agent 的 OpenId可选参数
* @throws ServiceException 修改失败时抛出异常(包括余额不足等情况)
*/
suspend fun updateAgentRule(id: Int, rule: String, openId: String? = null)
/**
* 删除 Agent 规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteAgentRule(id: Int)
/**
* 查询 Agent 规则列表
* @param openId Agent 的 OpenId
* @param keyword 关键词搜索,可选参数
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleList(
openId: String,
keyword: String? = null,
page: Int = 1,
pageSize: Int = 10
): AgentRuleListResult
/**
* 查询 Agent 规则配额使用情况
* @param openId Agent 的 OpenId
* @return 规则配额信息,包含基础配额、已购买配额、当前使用量等
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity
}
class AgentRuleServiceImpl : AgentRuleService {
private val gson = Gson()
override suspend fun createAgentRuleByOpenId(openId: String, rule: String) {
val body = CreateAgentRuleRequestBody(
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.createAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("创建 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun updateAgentRule(id: Int, rule: String, openId: String?) {
val body = UpdateAgentRuleRequestBody(
id = id,
rule = rule,
promptId = null,
openId = openId
)
val resp = ApiClient.api.updateAgentRule(body)
if (!resp.isSuccessful) {
val errorText = resp.errorBody()?.string()
try {
val err = gson.fromJson(errorText, InsufficientBalanceError::class.java)
if (err != null && err.code == 35600) {
throw ServiceException(err.message)
}
} catch (_: Exception) {
// ignore parse error
}
throw ServiceException("更新 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun deleteAgentRule(id: Int) {
val resp = ApiClient.api.deleteAgentRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除 Agent 规则失败: HTTP ${resp.code()}")
}
}
override suspend fun getAgentRuleList(
openId: String,
keyword: String?,
page: Int,
pageSize: Int
): AgentRuleListResult {
val resp = ApiClient.api.getAgentRuleList(
promptId = openId,
rule = keyword,
page = page,
pageSize = pageSize
)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则列表失败")
return data.toEntity()
}
override suspend fun getAgentRuleQuota(openId: String): AgentRuleQuotaEntity {
val resp = ApiClient.api.getAgentRuleQuota(openId)
val data = resp.body()?.data ?: throw ServiceException("获取 Agent 规则配额失败")
return data.toEntity()
}
private fun AgentRuleListResponse.toEntity(): AgentRuleListResult = AgentRuleListResult(
page = page,
pageSize = pageSize,
total = total,
list = list.map { it.toEntity() }
)
private fun AgentRule.toEntity(): AgentRuleEntity = AgentRuleEntity(
id = id,
rule = rule,
creator = creator,
creatorType = creatorType,
scope = scope,
agent = prompt.toEntity(),
createdAt = createdAt,
updatedAt = updatedAt,
)
private fun AgentRuleAgent.toEntity(): AgentRuleAgentInfo = AgentRuleAgentInfo(
id = id,
title = title,
avatar = avatar,
)
private fun AgentRuleQuota.toEntity(): AgentRuleQuotaEntity = AgentRuleQuotaEntity(
agentId = promptId,
agentTitle = promptTitle,
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent,
)
}

View File

@@ -9,7 +9,7 @@ import com.google.gson.annotations.SerializedName
data class ListContainer<T>( data class ListContainer<T>(
// 总数 // 总数
@SerializedName("total") @SerializedName("total")
val total: Int, val total: Long,
// 当前页 // 当前页
@SerializedName("page") @SerializedName("page")
val page: Int, val page: Int,

View File

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

View File

@@ -1,11 +1,16 @@
package com.aiosman.ravenow.data package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.data.api.CreateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
import com.aiosman.ravenow.data.api.RoomRuleQuota
import com.aiosman.ravenow.data.api.RoomRule
import com.aiosman.ravenow.data.api.RoomRuleCreator
import com.aiosman.ravenow.entity.CreatorEntity import com.aiosman.ravenow.entity.CreatorEntity
import com.aiosman.ravenow.entity.ProfileEntity
import com.aiosman.ravenow.entity.RoomEntity import com.aiosman.ravenow.entity.RoomEntity
import com.aiosman.ravenow.entity.RoomRuleEntity
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
import com.aiosman.ravenow.entity.UsersEntity import com.aiosman.ravenow.entity.UsersEntity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
@@ -106,6 +111,190 @@ data class Users(
} }
} }
/**
* 房间规则相关服务
*/
interface RoomService {
/**
* 创建房间规则
* @param rule 规则内容,不能为空
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @throws ServiceException 创建失败时抛出异常
*/
suspend fun createRoomRule(
rule: String,
roomId: Int? = null,
trtcId: String? = null
)
/**
* 修改房间规则
* @param id 规则ID
* @param rule 新的规则内容,不能为空
* @throws ServiceException 修改失败时抛出异常
*/
suspend fun updateRoomRule(
id: Int,
rule: String
)
/**
* 删除房间规则
* @param id 规则ID
* @throws ServiceException 删除失败时抛出异常
*/
suspend fun deleteRoomRule(id: Int)
/**
* 查询房间规则列表
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
* @return 规则列表响应,包含分页信息和规则列表
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleList(
roomId: Int? = null,
trtcId: String? = null,
page: Int = 1,
pageSize: Int = 10
): ListContainer<RoomRuleEntity>
/**
* 查询规则配额使用情况
* @param roomId 房间ID与 trtcId 二选一
* @param trtcId TRTC房间ID与 roomId 二选一
* @return 规则配额信息
* @throws ServiceException 查询失败时抛出异常
*/
suspend fun getRoomRuleQuota(
roomId: Int? = null,
trtcId: String? = null
): RoomRuleQuotaEntity
}
/**
* 房间规则服务实现类
*/
class RoomServiceImpl : RoomService {
override suspend fun createRoomRule(
rule: String,
roomId: Int?,
trtcId: String?
) {
val resp = ApiClient.api.createRoomRule(
CreateRoomRuleRequestBody(
rule = rule,
roomId = roomId,
trtcId = trtcId
)
)
if (!resp.isSuccessful) {
throw ServiceException("创建房间规则失败")
}
}
override suspend fun updateRoomRule(
id: Int,
rule: String
) {
val resp = ApiClient.api.updateRoomRule(
UpdateRoomRuleRequestBody(
id = id,
rule = rule
)
)
if (!resp.isSuccessful) {
throw ServiceException("修改房间规则失败")
}
}
override suspend fun deleteRoomRule(id: Int) {
val resp = ApiClient.api.deleteRoomRule(id)
if (!resp.isSuccessful) {
throw ServiceException("删除房间规则失败")
}
}
override suspend fun getRoomRuleList(
roomId: Int?,
trtcId: String?,
page: Int,
pageSize: Int
): ListContainer<RoomRuleEntity> {
val resp = ApiClient.api.getRoomRuleList(
roomId = roomId,
trtcId = trtcId,
page = page,
pageSize = pageSize
)
val body = resp.body() ?: throw ServiceException("获取房间规则列表失败")
return ListContainer(
list = body.list.map { it.toRoomRuleEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
override suspend fun getRoomRuleQuota(
roomId: Int?,
trtcId: String?
): RoomRuleQuotaEntity {
val resp = ApiClient.api.getRoomRuleQuota(
roomId = roomId,
trtcId = trtcId
)
val body = resp.body() ?: throw ServiceException("获取规则配额信息失败")
val data = body.data ?: throw ServiceException("规则配额数据为空")
return data.toRoomRuleQuotaEntity()
}
}
/**
* RoomRule 扩展函数,转换为 RoomRuleEntity
*/
fun RoomRule.toRoomRuleEntity(): RoomRuleEntity {
return RoomRuleEntity(
id = id,
rule = rule,
creator = creator?.toRoomRuleCreatorEntity(),
creatorType = creatorType,
roomId = roomId,
createdAt = createdAt,
updatedAt = updatedAt
)
}
/**
* RoomRuleCreator 扩展函数,转换为 RoomRuleCreatorEntity
*/
fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity {
return RoomRuleCreatorEntity(
id = id,
nickname = nickname,
avatar = avatar
)
}
/**
* RoomRuleQuota 扩展函数,转换为 RoomRuleQuotaEntity
*/
fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
return RoomRuleQuotaEntity(
baseMaxCount = baseMaxCount,
purchasedCount = purchasedCount,
totalMaxCount = totalMaxCount,
currentCount = currentCount,
remainingCount = remainingCount,
usagePercent = usagePercent
)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7,18 +7,13 @@ import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.DataContainer import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Part
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -160,7 +155,7 @@ class AgentBackend {
return if (authorId != null) { return if (authorId != null) {
// getAgent 返回 DataContainer<ListContainer<Agent>> // getAgent 返回 DataContainer<ListContainer<Agent>>
val dataContainer = val dataContainer =
body as com.aiosman.ravenow.data.DataContainer<com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent>> body as DataContainer<ListContainer<Agent>>
val listContainer = dataContainer.data val listContainer = dataContainer.data
ListContainer( ListContainer(
total = listContainer.total, total = listContainer.total,
@@ -171,7 +166,7 @@ class AgentBackend {
} else { } else {
// getMyAgent 返回 ListContainer<Agent> // getMyAgent 返回 ListContainer<Agent>
val listContainer = val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent> body as ListContainer<Agent>
ListContainer( ListContainer(
total = listContainer.total, total = listContainer.total,
page = pageNumber, page = pageNumber,
@@ -251,7 +246,7 @@ class AgentLoader : DataLoader<AgentEntity, AgentLoaderExtraArgs>() {
} else { } else {
// getMyAgent 返回 ListContainer<Agent> // getMyAgent 返回 ListContainer<Agent>
val listContainer = val listContainer =
body as com.aiosman.ravenow.data.ListContainer<com.aiosman.ravenow.data.Agent> body as ListContainer<Agent>
ListContainer( ListContainer(
list = listContainer.list.map { it.toAgentEntity() }, list = listContainer.list.map { it.toAgentEntity() },
total = listContainer.total, total = listContainer.total,

View File

@@ -9,7 +9,7 @@ import com.aiosman.ravenow.data.ListContainer
abstract class DataLoader<T,ET> { abstract class DataLoader<T,ET> {
var list: MutableList<T> = mutableListOf() var list: MutableList<T> = mutableListOf()
var page by mutableStateOf(1) var page by mutableStateOf(1)
var total by mutableStateOf(0) var total by mutableStateOf(0L)
var pageSize by mutableStateOf(10) var pageSize by mutableStateOf(10)
var hasNext by mutableStateOf(true) var hasNext by mutableStateOf(true)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)

View File

@@ -260,6 +260,38 @@ data class MomentImageEntity(
var height: Int? = null var height: Int? = null
) )
/**
* 动态视频
*/
data class MomentVideoEntity(
// 视频ID
val id: Long,
// 视频URL
val url: String,
// 原始文件名
val originalUrl: String? = null,
// 直接访问URL
val directUrl: String? = null,
// 视频缩略图URL
val thumbnailUrl: String? = null,
// 视频缩略图直接访问URL
val thumbnailDirectUrl: String? = null,
// 视频时长(秒)
val duration: Int? = null,
// 宽度
val width: Int? = null,
// 高度
val height: Int? = null,
// 文件大小(字节)
val size: Long? = null,
// 视频格式
val format: String? = null,
// 视频比特率kbps
val bitrate: Int? = null,
// 帧率
val frameRate: String? = null
)
/** /**
* 动态 * 动态
*/ */
@@ -299,12 +331,34 @@ data class MomentEntity(
// 关联动态 // 关联动态
var relMoment: MomentEntity? = null, var relMoment: MomentEntity? = null,
// 是否收藏 // 是否收藏
var isFavorite: Boolean = false var isFavorite: Boolean = false,
// 外部链接
val url: String? = null,
// 动态视频列表
val videos: List<MomentVideoEntity>? = null,
// 新闻相关字段
val isNews: Boolean = false,
val newsTitle: String = "",
val newsUrl: String = "",
val newsSource: String = "",
val newsCategory: String = "",
val newsLanguage: String = "",
val newsContent: String = "",
// 是否已获取完整正文
val hasFullText: Boolean = false,
// 新闻摘要
val summary: String? = null,
// 新闻发布时间
val publishedAt: String? = null,
// 是否已缓存图片
val imageCached: Boolean = false
) )
class MomentLoaderExtraArgs( class MomentLoaderExtraArgs(
val explore: Boolean? = false, val explore: Boolean? = false,
val timelineId: Int? = null, val timelineId: Int? = null,
val authorId : Int? = null val authorId : Int? = null,
val newsOnly: Boolean? = null,
val videoOnly: Boolean? = null
) )
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() { class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData( override suspend fun fetchData(
@@ -317,7 +371,9 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
pageSize = pageSize, pageSize = pageSize,
explore = if (extra.explore == true) "true" else "", explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId, timelineId = extra.timelineId,
authorId = extra.authorId authorId = extra.authorId,
newsFilter = if (extra.newsOnly == true) "news_only" else "",
videoFilter = if (extra.videoOnly == true) "video_only" else ""
) )
val data = result.body()?.let { val data = result.body()?.let {
ListContainer( ListContainer(
@@ -355,6 +411,18 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
onListChanged?.invoke(this.list) onListChanged?.invoke(this.list)
} }
fun updateCommentCount(id: Int, delta: Int) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
momentItem.copy(commentCount = newCount)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun removeMoment(id: Int) { fun removeMoment(id: Int) {
this.list = this.list.filter { it.id != id }.toMutableList() this.list = this.list.filter { it.id != id }.toMutableList()
onListChanged?.invoke(this.list) onListChanged?.invoke(this.list)

View File

@@ -3,10 +3,6 @@ package com.aiosman.ravenow.entity
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/** /**
* 群聊房间 * 群聊房间
@@ -56,6 +52,40 @@ data class ProfileEntity(
val aiAccount: Boolean, val aiAccount: Boolean,
) )
/**
* 房间规则创建者信息
*/
data class RoomRuleCreatorEntity(
val id: Int,
val nickname: String,
val avatar: String
)
/**
* 房间规则详情
*/
data class RoomRuleEntity(
val id: Int,
val rule: String,
val creator: RoomRuleCreatorEntity?,
val creatorType: String,
val roomId: Int,
val createdAt: String,
val updatedAt: String
)
/**
* 房间规则配额信息
*/
data class RoomRuleQuotaEntity(
val baseMaxCount: Int,
val purchasedCount: Int,
val totalMaxCount: Int,
val currentCount: Int,
val remainingCount: Int,
val usagePercent: Double
)
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() { class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
override suspend fun fetchData( override suspend fun fetchData(
page: Int, page: Int,

View File

@@ -29,6 +29,12 @@ object AppStore {
AppState.darkMode = true AppState.darkMode = true
AppState.appTheme = DarkThemeColors() AppState.appTheme = DarkThemeColors()
} }
// load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
if (savedBgUrl != null) {
AppState.chatBackgroundUrl = savedBgUrl
}
} }
@@ -54,5 +60,40 @@ object AppStore {
}.apply() }.apply()
} }
fun saveChatBackgroundUrl(url: String?) {
sharedPreferences.edit().apply {
if (url != null) {
putString("chatBackgroundUrl", url)
} else {
remove("chatBackgroundUrl")
}
}.apply()
AppState.chatBackgroundUrl = url
}
// ===================== 用户本地扩展信息 =====================
// 后端暂未提供 MBTI 与星座字段,使用本地持久化按用户维度进行存储
private fun mbtiKey(userId: Int) = "mbti_user_$userId"
private fun zodiacKey(userId: Int) = "zodiac_user_$userId"
fun getUserMbti(userId: Int): String? {
return sharedPreferences.getString(mbtiKey(userId), null)
}
fun setUserMbti(userId: Int, mbti: String?) {
sharedPreferences.edit().apply {
if (mbti.isNullOrEmpty()) remove(mbtiKey(userId)) else putString(mbtiKey(userId), mbti)
}.apply()
}
fun getUserZodiac(userId: Int): String? {
return sharedPreferences.getString(zodiacKey(userId), null)
}
fun setUserZodiac(userId: Int, zodiac: String?) {
sharedPreferences.edit().apply {
if (zodiac.isNullOrEmpty()) remove(zodiacKey(userId)) else putString(zodiacKey(userId), zodiac)
}.apply()
}
} }

View File

@@ -35,16 +35,20 @@ import com.aiosman.ravenow.LocalSharedTransitionScope
import com.aiosman.ravenow.ui.about.AboutScreen import com.aiosman.ravenow.ui.about.AboutScreen
import com.aiosman.ravenow.ui.account.AccountEditScreen2 import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.MbtiSelectScreen
import com.aiosman.ravenow.ui.account.RemoveAccountScreen import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
import com.aiosman.ravenow.ui.chat.ChatScreen import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen import com.aiosman.ravenow.ui.comment.CommentsScreen
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.composables.AgentCreatedSuccessIndicator
import com.aiosman.ravenow.ui.crop.ImageCropScreen import com.aiosman.ravenow.ui.crop.ImageCropScreen
import com.aiosman.ravenow.ui.favourite.FavouriteListPage import com.aiosman.ravenow.ui.favourite.FavouriteListPage
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
@@ -70,6 +74,7 @@ import com.aiosman.ravenow.ui.post.NewPostScreen
import com.aiosman.ravenow.ui.post.PostScreen import com.aiosman.ravenow.ui.post.PostScreen
import com.aiosman.ravenow.ui.profile.AccountProfileV2 import com.aiosman.ravenow.ui.profile.AccountProfileV2
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
import com.aiosman.ravenow.ui.notification.NotificationScreen
sealed class NavigationRoute( sealed class NavigationRoute(
val route: String, val route: String,
@@ -104,6 +109,7 @@ sealed class NavigationRoute(
data object FavouriteList : NavigationRoute("FavouriteList") data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}") data object Chat : NavigationRoute("Chat/{id}")
data object ChatAi : NavigationRoute("ChatAi/{id}") data object ChatAi : NavigationRoute("ChatAi/{id}")
data object ChatSetting : NavigationRoute("ChatSetting")
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}") data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen") data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop") data object ImageCrop : NavigationRoute("ImageCrop")
@@ -115,6 +121,9 @@ sealed class NavigationRoute(
data object GroupInfo : NavigationRoute("GroupInfo/{id}") data object GroupInfo : NavigationRoute("GroupInfo/{id}")
data object VipSelPage : NavigationRoute("VipSelPage") data object VipSelPage : NavigationRoute("VipSelPage")
data object RemoveAccountScreen: NavigationRoute("RemoveAccount") data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
data object NotificationScreen : NavigationRoute("NotificationScreen")
data object MbtiSelect : NavigationRoute("MbtiSelect")
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
} }
@@ -416,6 +425,12 @@ fun NavigationController(
composable(route = NavigationRoute.RemoveAccountScreen.route) { composable(route = NavigationRoute.RemoveAccountScreen.route) {
RemoveAccountScreen() RemoveAccountScreen()
} }
composable(route = NavigationRoute.MbtiSelect.route) {
MbtiSelectScreen()
}
composable(route = NavigationRoute.ZodiacSelect.route) {
ZodiacSelectScreen()
}
composable(route = NavigationRoute.VipSelPage.route) { composable(route = NavigationRoute.VipSelPage.route) {
VipSelPage() VipSelPage()
} }
@@ -488,6 +503,14 @@ fun NavigationController(
} }
} }
composable(route = NavigationRoute.ChatSetting.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ChatSettingScreen()
}
}
composable( composable(
route = NavigationRoute.ChatGroup.route, route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType }, arguments = listOf(navArgument("id") { type = NavType.StringType },
@@ -590,6 +613,13 @@ fun NavigationController(
} }
} }
composable(route = NavigationRoute.NotificationScreen.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
NotificationScreen()
}
}
} }
@@ -615,6 +645,7 @@ fun Navigation(
navController = navController, navController = navController,
startDestination = startDestination startDestination = startDestination
) )
AgentCreatedSuccessIndicator()
} }
} }
} }

View File

@@ -21,11 +21,16 @@ object AccountEditViewModel : ViewModel() {
var name by mutableStateOf("") var name by mutableStateOf("")
var bio by mutableStateOf("") var bio by mutableStateOf("")
var imageUrl by mutableStateOf<Uri?>(null) var imageUrl by mutableStateOf<Uri?>(null)
var bannerImageUrl by mutableStateOf<Uri?>(null)
var bannerFile by mutableStateOf<File?>(null)
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
var profile by mutableStateOf<AccountProfileEntity?>(null) var profile by mutableStateOf<AccountProfileEntity?>(null)
var croppedBitmap by mutableStateOf<Bitmap?>(null) var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false) var isUpdating by mutableStateOf(false)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
// 本地扩展字段
var mbti by mutableStateOf<String?>(null)
var zodiac by mutableStateOf<String?>(null)
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) { suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料") Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
isLoading = true isLoading = true
@@ -38,6 +43,12 @@ object AccountEditViewModel : ViewModel() {
bio = it.bio bio = it.bio
// 清除之前裁剪的图片 // 清除之前裁剪的图片
croppedBitmap = null croppedBitmap = null
// 读取本地扩展字段
try {
val uid = it.id // 使用 profile 的 id确保非空
mbti = com.aiosman.ravenow.AppStore.getUserMbti(uid)
zodiac = com.aiosman.ravenow.AppStore.getUserZodiac(uid)
} catch (_: Exception) { }
if (updateTrtcProfile) { if (updateTrtcProfile) {
TrtcHelper.updateTrtcProfile( TrtcHelper.updateTrtcProfile(
it.nickName, it.nickName,
@@ -73,6 +84,30 @@ object AccountEditViewModel : ViewModel() {
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream()) it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
UploadImage(file, "avatar.jpg", "", "jpg") UploadImage(file, "avatar.jpg", "", "jpg")
} }
// 处理背景图更新
val newBanner = bannerImageUrl?.let { uri ->
bannerFile?.let { file ->
val cursor = context.contentResolver.query(uri, null, null, null, null)
var uploadBanner: UploadImage? = null
cursor?.use { cur ->
val columnIndex = cur.getColumnIndex("_display_name")
if (cur.moveToFirst() && columnIndex != -1) {
val displayName = cur.getString(columnIndex)
val extension = displayName.substringAfterLast(".")
Log.d("AccountEditViewModel", "Banner file name: $displayName, extension: $extension")
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
} else {
// 如果无法获取文件名,使用默认值
val displayName = "banner.jpg"
val extension = "jpg"
uploadBanner = UploadImage(file, displayName, uri.toString(), extension)
}
}
uploadBanner
}
}
// 去除换行符,确保昵称和个人简介不包含换行 // 去除换行符,确保昵称和个人简介不包含换行
val cleanName = name.trim().replace("\n", "").replace("\r", "") val cleanName = name.trim().replace("\n", "").replace("\r", "")
val cleanBio = bio.trim().replace("\n", "").replace("\r", "") val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
@@ -80,10 +115,20 @@ object AccountEditViewModel : ViewModel() {
val newName = if (cleanName == profile?.nickName) null else cleanName val newName = if (cleanName == profile?.nickName) null else cleanName
accountService.updateProfile( accountService.updateProfile(
avatar = newAvatar, avatar = newAvatar,
banner = null, banner = newBanner,
nickName = newName, nickName = newName,
bio = cleanBio bio = cleanBio
) )
// 保存本地扩展字段
try {
profile?.id?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
}
} catch (_: Exception) { }
// 清除背景图状态
bannerImageUrl = null
bannerFile = null
// 刷新用户资料 // 刷新用户资料
reloadProfile() reloadProfile()
// 刷新个人资料页面的用户资料 // 刷新个人资料页面的用户资料
@@ -100,6 +145,8 @@ object AccountEditViewModel : ViewModel() {
name = "" name = ""
bio = "" bio = ""
imageUrl = null imageUrl = null
bannerImageUrl = null
bannerFile = null
croppedBitmap = null croppedBitmap = null
isUpdating = false isUpdating = false
isLoading = false isLoading = false

View File

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

View File

@@ -0,0 +1,141 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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
// MBTI类型列表
val MBTI_TYPES = listOf(
"INTJ", "INTP", "ENTJ", "ENTP",
"INFJ", "INFP", "ENFJ", "ENFP",
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
"ISTP", "ISFP", "ESTP", "ESFP"
)
@Composable
fun MbtiSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val model = AccountEditViewModel
val currentMbti = model.mbti
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_mbti),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(MBTI_TYPES) { mbti ->
MBTIItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun MBTIItem(
mbti: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

@@ -0,0 +1,172 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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
// 星座资源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 currentZodiacResId = findZodiacResId(model.zodiac)
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.choose_zodiac),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(ZODIAC_SIGN_RES_IDS.size) { index ->
val zodiacResId = ZODIAC_SIGN_RES_IDS[index]
val zodiacText = stringResource(zodiacResId)
ZodiacItem(
zodiac = zodiacText,
zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId,
onClick = {
// 保存当前语言的星座文本
model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
navController.navigateUp()
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
fun ZodiacItem(
zodiac: String,
zodiacResId: Int,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = zodiac,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

View File

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

View File

@@ -29,10 +29,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -61,7 +63,28 @@ import com.aiosman.ravenow.ui.composables.form.FormTextInput2
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.border
import androidx.compose.ui.draw.shadow
import com.aiosman.ravenow.AppState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.text.TextStyle
import com.aiosman.ravenow.ui.agent.AddAgentViewModel.showManualCreation
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
/** /**
* 添加智能体界面 * 添加智能体界面
*/ */
@@ -73,9 +96,13 @@ fun AddAgentScreen() {
var agnetNameError by remember { mutableStateOf<String?>(null) } var agnetNameError by remember { mutableStateOf<String?>(null) }
var agnetDescError by remember { mutableStateOf<String?>(null) } var agnetDescError by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
var isProcessing by remember { mutableStateOf(false) }
var showWaveAnimation by remember { mutableStateOf(false) }
var isCreatingAgent by remember { mutableStateOf(false) } // 控制是否处于创建状态
var showManualCreationForm by remember { mutableStateOf(false) } // 控制是否显示手动创建表单
var tempDesc by remember { mutableStateOf("") } // 独立的临时描述变量
val keyboardController = LocalSoftwareKeyboardController.current
fun onNameChange(value: String) { fun onNameChange(value: String) {
model.name = value.trim() model.name = value.trim()
agnetNameError = when { agnetNameError = when {
@@ -88,15 +115,37 @@ fun AddAgentScreen() {
fun onDescChange(value: String) { fun onDescChange(value: String) {
model.desc = value.trim() model.desc = value.trim()
agnetDescError = when { agnetDescError = when {
value.length > 100 -> "简介长度不能大于100" value.length > 512 -> "简介长度不能大于512"
else -> null
}
}
fun onTempDescChange(value: String) {
tempDesc = value.trim()
agnetDescError = when {
value.length > 512 -> "简介长度不能大于512"
else -> null else -> null
} }
} }
fun validate(): Boolean { fun validate(): Boolean {
return agnetNameError == null && agnetDescError == null return agnetNameError == null && agnetDescError == null
} }
// AI文案优化
suspend fun optimizeTextWithAI(content: String): String? {
return try {
val sessionId = ""
val response = com.aiosman.ravenow.data.api.ApiClient.api.agentMoment(
com.aiosman.ravenow.data.api.AgentMomentRequestBody(
generateText = content,
sessionId = sessionId
)
)
response.body()?.data
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// 处理系统返回键 // 处理系统返回键
BackHandler { BackHandler {
@@ -106,21 +155,39 @@ fun AddAgentScreen() {
} }
navController.popBackStack() navController.popBackStack()
} }
// 页面进入时重置头像选择状态 // 页面进入时重置头像选择状态
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.isSelectingAvatar = false model.isSelectingAvatar = false
// 根据标记恢复相应的状态
if (model.isAutoModeManualForm) {
// 恢复自动模式下的手动表单状态
showManualCreationForm = model.showManualCreationForm
isCreatingAgent = model.isCreatingAgent
showWaveAnimation = model.showWaveAnimation
showManualCreation = model.showManualCreation
} else {
// 恢复手动模式下的状态
showManualCreation = model.showManualCreation
showManualCreationForm = model.showManualCreationForm
isCreatingAgent = model.isCreatingAgent
showWaveAnimation = model.showWaveAnimation
}
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = appColors.decentBackground), .background(color = appColors.decentBackground),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
var showManualCreation by remember {
mutableStateOf(model.showManualCreation)
}
StatusBarSpacer() StatusBarSpacer()
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp)
.background(color = appColors.decentBackground) .background(color = appColors.decentBackground)
) { ) {
// 自定义header控制返回按钮行为 // 自定义header控制返回按钮行为
@@ -148,80 +215,500 @@ fun AddAgentScreen() {
stringResource(R.string.agent_add), stringResource(R.string.agent_add),
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
fontSize = 17.sp, fontSize = 17.sp,
color = appColors.text color = appColors.text
) )
Spacer(modifier = Modifier.size(12.dp)) }
Icon( }
Spacer(modifier = Modifier.height(1.dp))
if (!isCreatingAgent) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(horizontal = 20.dp),
) {
Image(
painter = painterResource(id = R.mipmap.group_copy),
contentDescription = "",
modifier = Modifier modifier = Modifier
.size(24.dp) .size(48.dp)
.noRippleClickable { .clip(
// 提交创建智能体的逻辑可以在这里实现 RoundedCornerShape(48.dp)
}, ),
imageVector = Icons.Default.Check, contentScale = ContentScale.Crop
contentDescription = "Add",
tint = appColors.text
) )
} }
} }
Spacer(modifier = Modifier.height(44.dp)) Spacer(modifier = Modifier.height(16.dp))
Box( Column(
modifier = Modifier.size(88.dp), modifier = Modifier.fillMaxWidth()
contentAlignment = Alignment.Center .padding(start = 20.dp)
) { ) {
CustomAsyncImage( Text(
context, text = "${AppState.profile?.nickName ?: "User"} ${stringResource(R.string.welcome_1)}",
model.croppedBitmap, fontSize = 16.sp,
modifier = Modifier color = appColors.text,
.size(88.dp) fontWeight = FontWeight.W600
.clip(
RoundedCornerShape(88.dp)
),
contentDescription = "",
contentScale = ContentScale.Crop,
placeholderRes = R.mipmap.rider_pro_agent_avatar
) )
}
if (!isCreatingAgent) {
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier.fillMaxWidth()
.padding(start = 20.dp)
) {
if (!showManualCreation) {
Text(
text = stringResource(R.string.welcome_2),
fontSize = 14.sp,
color = appColors.text.copy(alpha = 0.6f),
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
if (!showManualCreation) {
//自动创造AI界面
Column(
modifier = Modifier
.padding(horizontal = 20.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(32.dp) .fillMaxWidth()
.clip(CircleShape) .height(95.dp)
.background(appColors.main) .shadow(
.align(Alignment.BottomEnd) elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.inputBackground2,
shape = RoundedCornerShape(10.dp)
)
) {
val focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
model.viewModelScope.launch {
focusRequester.requestFocus()
keyboardController?.show()
}
}
)
TextField(
value = tempDesc,
onValueChange = { value -> onTempDescChange(value) },
modifier = Modifier
.fillMaxWidth()
.height(95.dp)
.focusRequester(focusRequester),
placeholder = {
Text(
text = stringResource(R.string.agent_desc_hint_auto),
color = Color.Gray
)
},
textStyle = TextStyle(
color = LocalAppTheme.current.text,
fontSize = 16.sp
),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedContainerColor = appColors.inputBackground2,
unfocusedContainerColor = appColors.inputBackground2,
),
shape = RoundedCornerShape(10.dp),
supportingText = null,
trailingIcon = null,
leadingIcon = null
)
Row(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.noRippleClickable {
// 只有在有内容且未处理中且未创建中时才可点击
if (tempDesc.isNotEmpty() && !isProcessing && !isCreatingAgent) {
isProcessing = true
showWaveAnimation = true // 显示构思动画
keyboardController?.hide()
model.viewModelScope.launch {
try {
val optimizedText = optimizeTextWithAI(tempDesc)
if (optimizedText != null) {
onTempDescChange(optimizedText)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
isProcessing = false
showWaveAnimation = false
isCreatingAgent = true
showManualCreationForm = true
onDescChange(tempDesc)
}
}
}
}
.then(
if (tempDesc.isEmpty() || isProcessing || isCreatingAgent) {
Modifier.alpha(0.5f)
} else {
Modifier
}
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = stringResource(R.string.agent_text_beautify),
color = Color(0xFF6246FF),
fontSize = 14.sp
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
if ((isCreatingAgent && showWaveAnimation) || (isProcessing && showWaveAnimation)) {
// 显示构思动画
Row(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(32.dp)
) {
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("loading.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.matchParentSize()
)
}
Text(
text = stringResource(R.string.ideaing),
color = appColors.text.copy(alpha = 0.6f),
fontSize = 14.sp
)
}
} else if (isCreatingAgent && !showWaveAnimation && showManualCreationForm) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.Start)
) {
Text(
text = stringResource(R.string.avatar),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.align(Alignment.Start)
.noRippleClickable {
// 保存当前状态
model.showManualCreationForm = showManualCreationForm
model.isCreatingAgent = isCreatingAgent
model.showWaveAnimation = showWaveAnimation
model.showManualCreation = showManualCreation
model.isAutoModeManualForm = true // 标记为自动模式下的手动表单
// 设置正在选择头像的标志
model.isSelectingAvatar = true
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
){
// 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
if (model.croppedBitmap != null) {
Image(
bitmap = model.croppedBitmap!!.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Edit",
tint = Color.White,
modifier = Modifier.size(20.dp),
)
}
}
Spacer(modifier = Modifier.height(18.dp))
// 原版两个输入框
Text(
text = stringResource(R.string.agent_name),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput(
value = model.name,
hint = stringResource(R.string.agent_name_hint_1),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onNameChange(value)
}
Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2(
value = model.desc,
hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onDescChange(value)
}
}
} else if (!isCreatingAgent && !showWaveAnimation) {
Box(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 20.dp)
.width(136.dp)
.height(40.dp)
.border(
width = 1.dp,
color = Color(0x33858B98),
shape = RoundedCornerShape(12.dp)
)
.background(
color = appColors.background,
shape = RoundedCornerShape(12.dp),
)
.noRippleClickable { .noRippleClickable {
showManualCreation = true
tempDesc = ""
agnetDescError = null
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 18.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.create_agent_hand),
color = appColors.text,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
}
}else {
//手动创造AI界面
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.align(Alignment.Start)
) {
Box(
modifier = Modifier
.align(Alignment.Start)
.width(140.dp)
.height(40.dp)
.shadow(
elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
spotColor = Color(0x33F563FF),
ambientColor = Color(0x99F563FF),
clip = false
)
.background(
brush = Brush.linearGradient(
listOf(
Color(0xFF6246FF),
Color(0xFF7C45ED)
)
),
shape = RoundedCornerShape(10.dp)
)
.padding(0.5.dp)
.background(
color = appColors.background,
shape = RoundedCornerShape(10.dp),
)
.noRippleClickable {
showManualCreation = false
model.name = ""
model.desc = ""
model.croppedBitmap = null
isCreatingAgent = false
showManualCreationForm = false
showWaveAnimation = false
isProcessing = false
}
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_info_magic),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.create_agent_auto),
color = appColors.text,
fontWeight = FontWeight.W600,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.avatar),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x777c45ed),
Color(0x777c68ef),
Color(0x557bd8f8)
)
)
)
.align(Alignment.Start)
.noRippleClickable {
// 保存当前状态
model.showManualCreation = showManualCreation
model.showManualCreationForm = showManualCreationForm
model.isCreatingAgent = isCreatingAgent
model.showWaveAnimation = showWaveAnimation
model.isAutoModeManualForm = false // 标记为手动模式下的手动表单
// 设置正在选择头像的标志 // 设置正在选择头像的标志
model.isSelectingAvatar = true model.isSelectingAvatar = true
navController.navigate(NavigationRoute.AgentImageCrop.route) navController.navigate(NavigationRoute.AgentImageCrop.route)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( // 如果已有裁剪后的头像,则显示头像,否则显示编辑图标
Icons.Default.Add, if (model.croppedBitmap != null) {
contentDescription = "Add", Image(
tint = Color.White, bitmap = model.croppedBitmap!!.asImageBitmap(),
) contentDescription = "Avatar",
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = "Edit",
tint = Color.White,
modifier = Modifier.size(20.dp),
)
}
} }
} }
Spacer(modifier = Modifier.height(58.dp)) Spacer(modifier = Modifier.height(18.dp))
// 原版两个输入框
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Text(
text = stringResource(R.string.agent_name),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput( FormTextInput(
value = model.name, value = model.name,
label = stringResource(R.string.agent_name), hint = stringResource(R.string.agent_name_hint_1),
hint = stringResource(R.string.agent_name_hint),
background = appColors.inputBackground2, background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { value -> ) { value ->
onNameChange(value) onNameChange(value)
} }
// Spacer(modifier = Modifier.height(16.dp)) Text(
text = stringResource(R.string.agent_desc),
fontSize = 12.sp,
color = appColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(4.dp))
FormTextInput2( FormTextInput2(
value = model.desc, value = model.desc,
label = stringResource(R.string.agent_desc),
hint = stringResource(R.string.agent_desc_hint), hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2, background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -229,38 +716,41 @@ fun AddAgentScreen() {
onDescChange(value) onDescChange(value)
} }
} }
Spacer(modifier = Modifier.height(58.dp)) //手动创造AI界面
}
// 错误信息显示
errorMessage?.let { error -> // 错误信息显示
Text( Spacer(modifier = Modifier.weight(1f))
text = error, Box(modifier = Modifier.fillMaxWidth()) {
color = Color.Red, errorMessage?.let { error ->
modifier = Modifier.padding(horizontal = 16.dp), Text(
fontSize = 14.sp text = error,
) color = Color.Red,
Spacer(modifier = Modifier.height(16.dp)) modifier = Modifier
.padding(bottom = 20.dp)
.align(Alignment.Center),
fontSize = 14.sp
)
}
} }
ActionButton( ActionButton(
modifier = Modifier modifier = Modifier
.width(345.dp) .width(345.dp)
.padding(horizontal = 16.dp) .padding(bottom = 40.dp)
.background( .background(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf( colors = listOf(
Color(0xFFEE2A33), Color(0x777c45ed),
Color(0xFFD80264), Color(0x777c68ef),
Color(0xFF8468BC) Color(0x557bd8f8)
) )
), ),
shape = RoundedCornerShape(24.dp) shape = RoundedCornerShape(24.dp)
), ),
color = Color.White, color = Color.White,
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
text = stringResource(R.string.agent_create), text = stringResource(R.string.create_confirm),
isLoading = model.isUpdating, isLoading = model.isUpdating,
loadingText = stringResource(R.string.agent_createing),
enabled = !model.isUpdating && validate() enabled = !model.isUpdating && validate()
) { ) {
// 验证输入 // 验证输入
@@ -268,12 +758,16 @@ fun AddAgentScreen() {
if (validationError != null) { if (validationError != null) {
// 显示验证错误 // 显示验证错误
errorMessage = validationError errorMessage = validationError
model.viewModelScope.launch {
kotlinx.coroutines.delay(3000)
errorMessage = null
}
return@ActionButton return@ActionButton
} }
// 清除之前的错误信息 // 清除之前的错误信息
errorMessage = null errorMessage = null
// 调用创建智能体API // 调用创建智能体API
model.viewModelScope.launch { model.viewModelScope.launch {
try { try {
@@ -282,15 +776,19 @@ fun AddAgentScreen() {
// 创建成功,清空数据并关闭页面 // 创建成功,清空数据并关闭页面
model.clearData() model.clearData()
navController.popBackStack() navController.popBackStack()
AppState.agentCreatedSuccess = true
} }
} catch (e: Exception) { } catch (e: Exception) {
// 显示错误信息 // 显示错误信息
errorMessage = "创建智能体失败: ${e.message}" errorMessage = "创建智能体失败: ${e.message}"
e.printStackTrace() e.printStackTrace()
// 3秒后清除错误信息
kotlinx.coroutines.delay(3000)
errorMessage = null
} }
} }
} }
} }
} }

View File

@@ -24,7 +24,12 @@ object AddAgentViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null) var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false) var isUpdating by mutableStateOf(false)
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像 var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
var showManualCreationForm by mutableStateOf(false)
var isCreatingAgent by mutableStateOf(false)
var showWaveAnimation by mutableStateOf(false)
var showManualCreation by mutableStateOf(false)
// 添加一个标志来区分两种手动表单状态
var isAutoModeManualForm by mutableStateOf(false)
suspend fun updateAgentAvatar(context: Context) { suspend fun updateAgentAvatar(context: Context) {
croppedBitmap?.let { croppedBitmap?.let {
val file = File(context.cacheDir, "agent_avatar.jpg") val file = File(context.cacheDir, "agent_avatar.jpg")
@@ -70,7 +75,7 @@ object AddAgentViewModel : ViewModel() {
name.length < 2 -> "智能体名称长度不能少于2个字符" name.length < 2 -> "智能体名称长度不能少于2个字符"
name.length > 20 -> "智能体名称长度不能超过20个字符" name.length > 20 -> "智能体名称长度不能超过20个字符"
desc.isEmpty() -> "智能体描述不能为空" desc.isEmpty() -> "智能体描述不能为空"
desc.length > 100 -> "智能体描述长度不能超过100个字符" desc.length > 512 -> "智能体描述长度不能超过512个字符"
else -> null else -> null
} }
} }
@@ -84,5 +89,10 @@ object AddAgentViewModel : ViewModel() {
croppedBitmap = null croppedBitmap = null
isUpdating = false isUpdating = false
isSelectingAvatar = false isSelectingAvatar = false
showManualCreationForm = false
isCreatingAgent = false
showWaveAnimation = false
showManualCreation = false
isAutoModeManualForm = false
} }
} }

View File

@@ -79,11 +79,12 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.ChatItem import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.exp.formatChatTime import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.AppState
import androidx.compose.ui.layout.ContentScale
import io.openim.android.sdk.enums.MessageType import io.openim.android.sdk.enums.MessageType
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -91,10 +92,10 @@ import java.util.UUID
@Composable @Composable
fun ChatAiScreen(userId: String) { fun ChatAiScreen(userId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalNavController.current.context val context = LocalNavController.current.context
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val chatBackgroundUrl = AppState.chatBackgroundUrl
var goToNewCount by remember { mutableStateOf(0) } var goToNewCount by remember { mutableStateOf(0) }
val viewModel = viewModel<ChatAiViewModel>( val viewModel = viewModel<ChatAiViewModel>(
key = "ChatAiViewModel_$userId", key = "ChatAiViewModel_$userId",
@@ -158,14 +159,25 @@ fun ChatAiScreen(userId: String) {
} }
Scaffold( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier if (chatBackgroundUrl != null && chatBackgroundUrl.isNotEmpty()) {
.fillMaxSize(), CustomAsyncImage(
topBar = { imageUrl = chatBackgroundUrl,
modifier = Modifier.fillMaxSize(),
contentDescription = "chat_background",
contentScale = ContentScale.Crop
)
}
Scaffold(
modifier = Modifier
.fillMaxSize(),
backgroundColor = Color.Transparent,
topBar = {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .background(Color.Transparent)
) { ) {
StatusBarSpacer() StatusBarSpacer()
Row( Row(
@@ -213,56 +225,32 @@ fun ChatAiScreen(userId: String) {
modifier = Modifier modifier = Modifier
.size(28.dp) .size(28.dp)
.noRippleClickable { .noRippleClickable {
isMenuExpanded = true navController.navigate(NavigationRoute.ChatSetting.route)
}, },
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
) )
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "Unmute" else "Mute",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
) {
isMenuExpanded = false
if (NetworkUtils.isNetworkAvailable(context)) {
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
} else {
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
}
}
),
)
} }
} }
} }
}, },
bottomBar = { bottomBar = {
val hasChatBackground = AppState.chatBackgroundUrl != null && AppState.chatBackgroundUrl!!.isNotEmpty()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.imePadding() .imePadding()
) { ) {
Box( if (!hasChatBackground) {
modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.height(1.dp) .fillMaxWidth()
.background( .height(1.dp)
AppColors.decentBackground) .background(
) AppColors.decentBackground)
)
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
ChatAiInput( ChatAiInput(
onSendImage = { onSendImage = {
@@ -283,7 +271,7 @@ fun ChatAiScreen(userId: String) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.decentBackground) .background(Color.Transparent)
.padding(paddingValues) .padding(paddingValues)
) { ) {
LazyColumn( LazyColumn(
@@ -346,8 +334,7 @@ fun ChatAiScreen(userId: String) {
} }
} }
} }
}
} }
} }
@@ -571,7 +558,8 @@ fun ChatAiInput(
} }
Box( modifier = Modifier Box( modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){ .background(Color.Transparent)
.padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
Row( Row(
modifier = Modifier modifier = Modifier

View File

@@ -64,6 +64,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -74,10 +75,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.ChatItem import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.exp.formatChatTime import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem import com.aiosman.ravenow.ui.composables.MenuItem
@@ -91,7 +94,6 @@ import java.util.UUID
@Composable @Composable
fun ChatScreen(userId: String) { fun ChatScreen(userId: String) {
var isMenuExpanded by remember { mutableStateOf(false) }
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalNavController.current.context val context = LocalNavController.current.context
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
@@ -158,14 +160,37 @@ fun ChatScreen(userId: String) {
} }
Scaffold( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize()
) {
// 背景图层
val bgUrl = AppState.chatBackgroundUrl
if (bgUrl != null) {
CustomAsyncImage(
imageUrl = bgUrl,
modifier = Modifier.fillMaxSize(),
contentDescription = "chat_background",
contentScale = ContentScale.Crop
)
} else {
// 无背景时使用主题背景色
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
)
}
Scaffold(
modifier = Modifier
.fillMaxSize(),
backgroundColor = Color.Transparent,
topBar = { topBar = {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .background(Color.Transparent)
) { ) {
StatusBarSpacer() StatusBarSpacer()
Row( Row(
@@ -214,39 +239,12 @@ fun ChatScreen(userId: String) {
modifier = Modifier modifier = Modifier
.size(28.dp) .size(28.dp)
.noRippleClickable { .noRippleClickable {
isMenuExpanded = true navController.navigate(NavigationRoute.ChatSetting.route)
}, },
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
) )
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
menuItems = listOf(
MenuItem(
title = if (viewModel.notificationStrategy == "mute") "取消静音" else "静音",
icon = if (viewModel.notificationStrategy == "mute") R.drawable.rider_pro_notice_mute else R.drawable.rider_pro_notice_active,
) {
isMenuExpanded = false
if (NetworkUtils.isNetworkAvailable(context)) {
viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active")
} else {
viewModel.updateNotificationStrategy("mute")
}
}
} else {
android.widget.Toast.makeText(context, "网络连接异常,请检查网络设置", android.widget.Toast.LENGTH_SHORT).show()
}
}
),
)
} }
} }
} }
@@ -279,11 +277,11 @@ fun ChatScreen(userId: String) {
} }
} }
} }
) { paddingValues -> ) { paddingValues ->
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.background) .background(Color.Transparent)
.padding(paddingValues) .padding(paddingValues)
) { ) {
LazyColumn( LazyColumn(
@@ -347,6 +345,7 @@ fun ChatScreen(userId: String) {
} }
}
} }
} }
@@ -572,7 +571,7 @@ fun ChatInput(
} }
Box( modifier = Modifier Box( modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),){ .padding(start = 16.dp, end = 16.dp, bottom = 45.dp),){
Row( Row(
modifier = Modifier modifier = Modifier

View File

@@ -0,0 +1,362 @@
package com.aiosman.ravenow.ui.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
@Composable
fun ChatSettingScreen() {
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
var showThemeSheet by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.secondaryBackground)
) {
StatusBarSpacer()
Box(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
NoticeScreenHeader(title = stringResource(R.string.chat_settings), moreIcon = false)
}
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
SettingCard(
title = stringResource(R.string.chat_theme_settings),
onClick = { showThemeSheet = true }
)
Spacer(modifier = Modifier.height(12.dp))
SettingCard(
title = stringResource(R.string.report),
onClick = { /* TODO: 跳转举报 */ }
)
}
}
if (showThemeSheet) {
ThemePickerSheet(onClose = { showThemeSheet = false })
}
}
@Composable
private fun SettingCard(title: String, onClick: () -> Unit) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(appColors.background)
.clickable { onClick() }
.padding(horizontal = 12.dp, vertical = 14.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
text = title,
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(id = R.drawable.rave_now_nav_right),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(20.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ThemePickerSheet(onClose: () -> Unit) {
val appColors = LocalAppTheme.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.9f
var previewUrl by remember { mutableStateOf<String?>(null) }
ModalBottomSheet(
onDismissRequest = onClose,
sheetState = sheetState,
containerColor = appColors.secondaryBackground,
dragHandle = null,
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.custom_background),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f).padding(start = 90.dp),
)
IconButton(onClick = onClose) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "close",
tint = appColors.text
)
}
}
// 从相册选择
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(appColors.background)
.clickable { /* TODO: 打开相册选择 */ }
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.select_from_gallery), color = appColors.text, fontSize = 15.sp, modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.group_info_edit),
contentDescription = null,
tint = appColors.text,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(text = stringResource(R.string.featured_backgrounds), color = appColors.text, fontSize = 12.sp)
Spacer(modifier = Modifier.height(8.dp))
val presets = remember {
listOf(
"https://picsum.photos/seed/ai1/400/600",
"https://picsum.photos/seed/ai2/400/600",
"https://picsum.photos/seed/ai3/400/600",
"https://picsum.photos/seed/ai4/400/600",
"https://picsum.photos/seed/ai5/400/600",
"https://picsum.photos/seed/ai6/400/600",
"https://picsum.photos/seed/ai7/400/600",
"https://picsum.photos/seed/ai8/400/600",
"https://picsum.photos/seed/ai9/400/600",
"https://picsum.photos/seed/ai10/400/600",
"https://picsum.photos/seed/ai11/400/600",
"https://picsum.photos/seed/ai12/400/600",
"https://picsum.photos/seed/ai13/400/600",
"https://picsum.photos/seed/ai14/400/600",
"https://picsum.photos/seed/ai15/400/600",
)
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
) {
items(presets) { url ->
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { previewUrl = url }
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(appColors.decentBackground)
) {
CustomAsyncImage(
imageUrl = url,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 4f),
contentDescription = "preset",
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Heart Drive",
color = appColors.text,
fontSize = 12.sp
)
}
}
}
// 预览自定义背景弹窗
if (previewUrl != null) {
ModalBottomSheet(
onDismissRequest = { previewUrl = null },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = appColors.secondaryBackground,
dragHandle = null,
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
) {
Box(modifier = Modifier.fillMaxSize()) {
CustomAsyncImage(
imageUrl = previewUrl!!,
modifier = Modifier.fillMaxSize(),
contentDescription = "preview_bg",
contentScale = ContentScale.Crop
)
Column(modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 8.dp)) {
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
Text(text = stringResource(R.string.previewing_custom_background), color = Color.White, fontSize = 15.sp)
}
Column(modifier = Modifier.padding(8.dp)) {
Row {
Box(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color.White)
.padding(horizontal = 14.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.each_theme_unique_experience), color = Color.Black, fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row {
Box(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color.White)
.padding(horizontal = 14.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.select_apply_to_use_theme), color = Color.Black, fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(12.dp))
Row {
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF7C4DFF))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(text = stringResource(R.string.tap_cancel_to_preview_other_themes), color = Color.White, fontSize = 12.sp)
}
}
}
Spacer(modifier = Modifier.weight(1f))
// 底部按钮
Row(
modifier = Modifier.fillMaxWidth()
.padding(bottom = 60.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(Color.White)
.clickable { previewUrl = null }
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.cancel), color = Color.Black, fontSize = 14.sp)
}
Spacer(modifier = Modifier.size(12.dp))
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xFFEE2A33),
Color(0xFFD80264),
Color(0xFF664C92)
)
)
)
.clickable {
previewUrl?.let { url ->
com.aiosman.ravenow.AppStore.saveChatBackgroundUrl(url)
previewUrl = null
onClose()
}
}
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.moment_ai_apply), color = Color.White, fontSize = 14.sp)
}
}
}
}
}
}
}
}
}

View File

@@ -47,7 +47,8 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@Composable @Composable
fun CommentNoticeScreen() { fun CommentNoticeScreen() {
val viewModel = viewModel<CommentNoticeListViewModel>( val viewModel = viewModel<CommentNoticeListViewModel>(
@@ -71,14 +72,47 @@ fun CommentNoticeScreen() {
modifier = Modifier.fillMaxSize().background(color = AppColors.background) modifier = Modifier.fillMaxSize().background(color = AppColors.background)
) { ) {
StatusBarSpacer() StatusBarSpacer()
Box( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
modifier = Modifier
.fillMaxWidth() if (!isNetworkAvailable) {
.padding(16.dp) Box(
) { modifier = Modifier
NoticeScreenHeader(stringResource(R.string.comment), moreIcon = false) .fillMaxSize()
} .padding(top = 149.dp),
if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) { contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
viewModel.initData(context, force = true)
}
)
}
}
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -91,12 +125,13 @@ fun CommentNoticeScreen() {
) { ) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_pl_qs_as_img id = if(AppState.darkMode) R.mipmap.tietie_dark
else R.mipmap.qst_pl_qs_img), else R.mipmap.invalid_name_11),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = "等一位旅人~", text = "等一位旅人~",
color = AppColors.text, color = AppColors.text,

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -32,6 +33,7 @@ fun ActionButton(
text: String, text: String,
color: Color? = null, color: Color? = null,
backgroundColor: Color? = null, backgroundColor: Color? = null,
backgroundBrush: Brush? = null,
leading: @Composable (() -> Unit)? = null, leading: @Composable (() -> Unit)? = null,
expandText: Boolean = false, expandText: Boolean = false,
contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), contentPadding: PaddingValues = PaddingValues(vertical = 16.dp),
@@ -65,7 +67,11 @@ fun ActionButton(
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(roundCorner.dp)) .clip(RoundedCornerShape(roundCorner.dp))
.background(animatedBackgroundColor) .background(
brush = backgroundBrush ?: Brush.linearGradient(
colors = listOf(animatedBackgroundColor, animatedBackgroundColor)
)
)
.noRippleClickable { .noRippleClickable {
if (enabled && !isLoading) { if (enabled && !isLoading) {
click() click()

View File

@@ -0,0 +1,75 @@
package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffoldScopeInstance.align
import kotlinx.coroutines.delay
@Composable
fun AgentCreatedSuccessIndicator() {
val appColors = LocalAppTheme.current
if (AppState.agentCreatedSuccess) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 70.dp),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.width(150.dp)
.height(40.dp)
.background(appColors.text.copy(alpha = 0.5f), shape = RoundedCornerShape(15.dp)),
contentAlignment = Alignment.CenterStart
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) {
Icon(
painter = painterResource(id = R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = Color.Unspecified
)
Spacer(modifier = Modifier.width(7.dp))
Text(
text = stringResource(R.string.create_success),
color = appColors.background,
fontSize = 13.sp
)
}
}
}
LaunchedEffect(Unit) {
delay(3000)
AppState.agentCreatedSuccess = false
}
}
}

View File

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

View File

@@ -56,15 +56,14 @@ fun AnimatedFavouriteIcon(
}) { }) {
Image( Image(
painter = if (isFavourite) { painter = if (isFavourite) {
painterResource(id = R.drawable.rider_pro_favourited) painterResource(id = R.mipmap.icon_variant_2)
} else { } else {
painterResource(id = R.drawable.rider_pro_favourite) painterResource(id = R.mipmap.icon_collect)
}, },
contentDescription = "Favourite", contentDescription = "Favourite",
modifier = modifier.graphicsLayer { modifier = modifier.graphicsLayer {
rotationZ = animatableRotation.value rotationZ = animatableRotation.value
}, },
colorFilter = ColorFilter.tint(AppColors.text)
) )
} }
} }

View File

@@ -51,7 +51,8 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
fun EditCommentBottomModal( fun EditCommentBottomModal(
replyComment: CommentEntity? = null, replyComment: CommentEntity? = null,
onSend: (String) -> Unit = {} autoFocus: Boolean = false,
onSend: (String) -> Unit = {},
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var text by remember { mutableStateOf("") } var text by remember { mutableStateOf("") }
@@ -59,8 +60,10 @@ fun EditCommentBottomModal(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(autoFocus) {
focusRequester.requestFocus() if (autoFocus) {
focusRequester.requestFocus()
}
} }
Column( Column(
@@ -69,72 +72,22 @@ fun EditCommentBottomModal(
.background(AppColors.background) .background(AppColors.background)
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
if (replyComment == null) "Comment" else "Reply",
fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f),
fontSize = 20.sp,
fontStyle = FontStyle.Italic,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(16.dp))
if (replyComment != null) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
) {
CustomAsyncImage(
context,
replyComment.avatar,
modifier = Modifier.fillMaxSize(),
contentDescription = "Avatar",
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
replyComment.name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
replyComment.comment,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp),
overflow = TextOverflow.Ellipsis,
color = AppColors.text
)
Spacer(modifier = Modifier.height(16.dp))
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(Color.White) .background(Color.Gray.copy(alpha = 0.1f))
.border(1.dp, Color.Black, RoundedCornerShape(20.dp))
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.Top modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
BasicTextField( BasicTextField(
value = text, value = text,
@@ -149,31 +102,40 @@ fun EditCommentBottomModal(
color = Color.Black, color = Color.Black,
fontWeight = FontWeight.Normal fontWeight = FontWeight.Normal
), ),
minLines = 1 decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
innerTextField()
if (text.isEmpty()) {
Text(
text = if (replyComment == null) "快来互动吧..." else "回复@${replyComment.name}",
color = AppColors.text.copy(alpha = 0.3f), // 30%透明度
)
}
}
}
) )
Spacer(modifier = Modifier.width(8.dp))
Crossfade(
targetState = text.isNotEmpty(), animationSpec = tween(500),
label = ""
) { isNotEmpty ->
Icon(
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
contentDescription = "Send",
modifier = Modifier
.size(20.dp)
.align(Alignment.Top)
.noRippleClickable {
if (text.isNotEmpty()) {
onSend(text)
text = ""
}
},
tint = if (isNotEmpty) Color.Unspecified else AppColors.nonActive
)
}
} }
} }
} Spacer(modifier = Modifier.width(12.dp))
Icon(
painter = painterResource(id = R.mipmap.btn),
contentDescription = "Send",
modifier = Modifier
.size(40.dp)
.padding(top = 13.dp)
.noRippleClickable {
if (text.isNotEmpty()) {
onSend(text)
text = ""
}
},
tint = Color.Unspecified
)
}
Spacer(modifier = Modifier.height(navBarHeight)) Spacer(modifier = Modifier.height(navBarHeight))
} }
} }

View File

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

View File

@@ -92,15 +92,21 @@ fun MomentCard(
showFollowButton = showFollowButton showFollowButton = showFollowButton
) )
} }
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.noRippleClickable { .noRippleClickable {
navController.navigateToPost( val currentTime = System.currentTimeMillis()
momentEntity.id, if (currentTime - lastClickTime.value > clickDelay) {
highlightCommentId = 0, lastClickTime.value = currentTime
initImagePagerIndex = imageIndex navController.navigateToPost(
) momentEntity.id,
highlightCommentId = 0,
initImagePagerIndex = imageIndex
)
}
} }
) { ) {
MomentContentGroup( MomentContentGroup(
@@ -213,8 +219,7 @@ fun MomentPostLocation(location: String) {
text = location, text = location,
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 12.sp, fontSize = 12.sp,
)
)
} }
@Composable @Composable
@@ -238,6 +243,8 @@ fun MomentTopRowGroup(
Row( Row(
modifier = Modifier modifier = Modifier
) { ) {
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
CustomAsyncImage( CustomAsyncImage(
context, context,
momentEntity.avatar, momentEntity.avatar,
@@ -246,12 +253,16 @@ fun MomentTopRowGroup(
.size(40.dp) .size(40.dp)
.clip(RoundedCornerShape(40.dp)) .clip(RoundedCornerShape(40.dp))
.noRippleClickable { .noRippleClickable {
navController.navigate( val currentTime = System.currentTimeMillis()
NavigationRoute.AccountProfile.route.replace( if (currentTime - lastClickTime.value > clickDelay) {
"{id}", lastClickTime.value = currentTime
momentEntity.authorId.toString() navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
momentEntity.authorId.toString()
)
) )
) }
}, },
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@@ -267,7 +278,19 @@ fun MomentTopRowGroup(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MomentName( MomentName(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f)
.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
momentEntity.authorId.toString()
)
)
}
},
name = momentEntity.nickname name = momentEntity.nickname
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -341,16 +364,6 @@ fun MomentContentGroup(
onPageChange: (Int) -> Unit = {} onPageChange: (Int) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
if (momentEntity.momentTextContent.isNotEmpty()) {
Text(
text = momentEntity.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
fontSize = 16.sp,
color = AppColors.text
)
}
if (momentEntity.relMoment != null) { if (momentEntity.relMoment != null) {
RelPostCard( RelPostCard(
momentEntity = momentEntity.relMoment!!, momentEntity = momentEntity.relMoment!!,
@@ -366,6 +379,17 @@ fun MomentContentGroup(
) )
} }
} }
if (momentEntity.momentTextContent.isNotEmpty()) {
Text(
text = momentEntity.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
fontSize = 16.sp,
color = AppColors.text
)
}
} }
@@ -378,14 +402,15 @@ fun MomentOperateBtn(@DrawableRes icon: Int, count: String) {
.size(width = 24.dp, height = 24.dp), .size(width = 24.dp, height = 24.dp),
painter = painterResource(id = icon), painter = painterResource(id = icon),
contentDescription = "", contentDescription = "",
colorFilter = ColorFilter.tint(AppColors.text)
)
Text(
text = count,
modifier = Modifier.padding(start = 7.dp),
fontSize = 14.sp,
color = AppColors.text
) )
if (count.isNotEmpty()) {
Text(
text = count,
modifier = Modifier.padding(start = 7.dp),
fontSize = 14.sp,
color = AppColors.text
)
}
} }
} }
@@ -401,7 +426,6 @@ fun MomentOperateBtn(count: String, content: @Composable () -> Unit) {
fontSize = 14, fontSize = 14,
modifier = Modifier modifier = Modifier
.padding(start = 7.dp) .padding(start = 7.dp)
.width(24.dp)
) )
} }
} }
@@ -416,6 +440,8 @@ fun MomentBottomOperateRowGroup(
momentEntity: MomentEntity, momentEntity: MomentEntity,
imageIndex: Int = 0 imageIndex: Int = 0
) { ) {
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
if (showCommentModal) { if (showCommentModal) {
ModalBottomSheet( ModalBottomSheet(
@@ -451,45 +477,84 @@ fun MomentBottomOperateRowGroup(
.height(56.dp) .height(56.dp)
.padding(start = 16.dp, end = 0.dp) .padding(start = 16.dp, end = 0.dp)
) { ) {
Row( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Box( if (momentEntity.images.size > 1) {
modifier = Modifier.fillMaxHeight(), Row(
contentAlignment = Alignment.Center modifier = Modifier
) { .fillMaxWidth()
MomentOperateBtn(count = momentEntity.likeCount.toString()) { .weight(1f),
AnimatedLikeIcon( horizontalArrangement = Arrangement.Center,
modifier = Modifier.size(24.dp), verticalAlignment = Alignment.CenterVertically
liked = momentEntity.liked ) {
) { momentEntity.images.forEachIndexed { index, _ ->
onLikeClick() Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (imageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
)
.padding(1.dp)
)
Spacer(modifier = Modifier.width(8.dp))
} }
} }
} }
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.fillMaxHeight()
.noRippleClickable {
onCommentClick()
},
contentAlignment = Alignment.Center
) {
MomentOperateBtn(
icon = R.drawable.rider_pro_comment,
count = momentEntity.commentCount.toString()
)
}
Box( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .weight(1f),
, verticalAlignment = Alignment.CenterVertically
contentAlignment = Alignment.CenterEnd
) { ) {
Row(
modifier = Modifier.weight(1f).fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
// 点赞按钮
MomentOperateBtn(count = momentEntity.likeCount.toString()) {
AnimatedLikeIcon(
modifier = Modifier.size(24.dp),
liked = momentEntity.liked
) {
onLikeClick()
}
}
Spacer(modifier = Modifier.width(10.dp))
// 评论按钮
Box(
modifier = Modifier.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
onCommentClick()
}
}
) {
MomentOperateBtn(
icon = R.mipmap.icon_comment,
count = momentEntity.commentCount.toString()
)
}
Spacer(modifier = Modifier.width(28.dp))
// 转发按钮
Box(
modifier = Modifier.noRippleClickable {
// TODO: 实现转发功能
}
) {
MomentOperateBtn(
icon = R.mipmap.icon_share,
count = ""
)
}
}
// 收藏按钮
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) { MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
AnimatedFavouriteIcon( AnimatedFavouriteIcon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
@@ -500,41 +565,18 @@ fun MomentBottomOperateRowGroup(
} }
} }
} }
if (momentEntity.images.size > 1) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
momentEntity.images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (imageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
)
.padding(1.dp)
)
Spacer(modifier = Modifier.width(8.dp))
}
}
}
} }
} }
@Composable @Composable
fun MomentListLoading() { fun MomentListLoading() {
CircularProgressIndicator( CircularProgressIndicator(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally), .wrapContentWidth(Alignment.CenterHorizontally),
color = Color.Red color = Color.Red
) )
} }

View File

@@ -1,10 +1,15 @@
package com.aiosman.ravenow.ui.composables package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -12,11 +17,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
/** /**
* 可复用的标签页组件 * 可复用的标签页组件
*/ */
@@ -54,3 +62,43 @@ fun TabItem(
fun TabSpacer() { fun TabSpacer() {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
@Composable
fun UnderlineTabItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.noRippleClickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = text,
fontSize = 15.sp,
fontWeight = FontWeight.ExtraBold,
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 13.dp)
)
// 选中状态下显示图标
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
if (isSelected) {
Image(
painter = painterResource(id = R.mipmap.underline),
contentDescription = "selected indicator",
)
}
}
}
}

View File

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

View File

@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
/** /**
* 水平布局的输入框 * 水平布局的输入框
@@ -47,12 +54,15 @@ fun FormTextInput(
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Column( Column(
modifier = modifier modifier = modifier
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(25.dp))
.background(background ?: AppColors.inputBackground) .background(background ?: AppColors.inputBackground)
.let { .let {
if (error != null) { if (error != null) {
@@ -61,7 +71,11 @@ fun FormTextInput(
it it
} }
} }
.padding(17.dp), .padding(17.dp)
.noRippleClickable {
focusRequester.requestFocus()
keyboardController?.show()
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
label?.let { label?.let {
@@ -79,34 +93,51 @@ fun FormTextInput(
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 16.dp)
) { ) {
if (value.isEmpty()) { Icon(
Text( painter = painterResource(id = R.mipmap.icons_infor_edit),
text = hint ?: "", contentDescription = null,
style = TextStyle( modifier = Modifier
fontSize = 16.sp, .size(16.dp)
fontWeight = FontWeight.Normal, .align(Alignment.TopStart),
color = AppColors.inputHint tint = AppColors.text.copy(alpha = 0.4f)
)
)
}
BasicTextField(
maxLines = 1,
value = value,
onValueChange = {
onValueChange(it)
},
singleLine = true,
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = AppColors.text
),
cursorBrush = SolidColor(AppColors.text),
) )
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier.weight(1f)
) {
if (value.isEmpty()) {
Text(
text = hint ?: "",
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = AppColors.inputHint
)
)
}
BasicTextField(
maxLines = 1,
value = value,
onValueChange = {
onValueChange(it)
},
modifier = Modifier
.focusRequester(focusRequester),
singleLine = true,
textStyle = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = AppColors.text
),
cursorBrush = SolidColor(AppColors.text),
)
}
}
} }
@@ -139,4 +170,4 @@ fun FormTextInput(
} }
} }
} }

View File

@@ -15,16 +15,22 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -32,6 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
/** /**
* 垂直布局的输入框 * 垂直布局的输入框
@@ -44,15 +51,19 @@ fun FormTextInput2(
error: String? = null, error: String? = null,
hint: String? = null, hint: String? = null,
background: Color? = null, background: Color? = null,
focusRequester: FocusRequester? = null,
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val localFocusRequester = focusRequester ?: remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
Column( Column(
modifier = modifier.height(150.dp) modifier = modifier.height(150.dp)
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(25.dp))
.background(background ?: AppColors.inputBackground) .background(background ?: AppColors.inputBackground)
.let { .let {
if (error != null) { if (error != null) {
@@ -61,9 +72,13 @@ fun FormTextInput2(
it it
} }
} }
.padding(17.dp), .padding(17.dp)
.noRippleClickable {
localFocusRequester.requestFocus()
keyboardController?.show()
},
) { ) {
label?.let { label?.let {
Text( Text(
text = it, text = it,
@@ -79,34 +94,51 @@ fun FormTextInput2(
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(top = 8.dp)
) { ) {
if (value.isEmpty()) { Icon(
Text( painter = painterResource(id = R.mipmap.icons_infor_edit),
text = hint ?: "", contentDescription = null,
style = TextStyle( modifier = Modifier
fontSize = 16.sp, .size(16.dp)
fontWeight = FontWeight.Normal, .align(Alignment.TopStart),
color = AppColors.inputHint tint = AppColors.text.copy(alpha = 0.4f)
)
)
}
BasicTextField(
maxLines = 5,
value = value,
onValueChange = {
onValueChange(it)
},
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = AppColors.text,
lineHeight = 20.sp
),
cursorBrush = SolidColor(AppColors.text),
) )
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier.weight(1f)
) {
if (value.isEmpty()) {
Text(
text = hint ?: "",
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = AppColors.inputHint
)
)
}
BasicTextField(
maxLines = 6,
value = value,
onValueChange = {
onValueChange(it)
},
modifier = Modifier
.focusRequester(localFocusRequester),
textStyle = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = AppColors.text,
lineHeight = 20.sp
),
cursorBrush = SolidColor(AppColors.text),
)
}
}
} }

View File

@@ -4,18 +4,23 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -24,7 +29,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
@@ -35,6 +42,8 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -71,55 +80,120 @@ fun FavouriteListPage() {
) { ) {
NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.favourites_upper), moreIcon = false)
} }
LazyVerticalGrid( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
columns = GridCells.Fixed(3), var moments = dataFlow.collectAsLazyPagingItems()
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
) { if (!isNetworkAvailable) {
items(moments.itemCount) { idx -> Box(
val momentItem = moments[idx] ?: return@items modifier = Modifier
Box( .fillMaxSize()
modifier = Modifier .padding(top=149.dp),
.fillMaxWidth() contentAlignment = Alignment.TopCenter
.aspectRatio(1f) ) {
.padding(2.dp) Column(
.noRippleClickable { horizontalAlignment = Alignment.CenterHorizontally,
navController.navigateToPost( modifier = Modifier.fillMaxWidth()
id = momentItem.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
) { ) {
CustomAsyncImage( Image(
imageUrl = momentItem.images[0].thumbnail, painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "", contentDescription = "network error",
modifier = Modifier modifier = Modifier.size(181.dp)
.fillMaxSize()
.clip(RoundedCornerShape(8.dp)),
context = context
) )
if (momentItem.images.size > 1) { Spacer(modifier = Modifier.size(24.dp))
Box( Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.refreshPager(force = true)
}
)
}
}
} else if(moments.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=189.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.invalid_dark
else R.mipmap.invalid_name_1),
contentDescription = "No favourites",
modifier = Modifier.size(181.dp, 153.dp)
)
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
Text(
text = stringResource(R.string.favourites_null),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}else{
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
) {
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.padding(2.dp)
.noRippleClickable {
navController.navigateToPost(
id = momentItem.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .fillMaxSize()
.align(Alignment.TopEnd) .clip(RoundedCornerShape(8.dp)),
) { context = context
Image( )
modifier = Modifier.size(24.dp), if (momentItem.images.size > 1) {
painter = painterResource(R.drawable.rider_pro_picture_more), Box(
contentDescription = "", modifier = Modifier
) .padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Image(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.rider_pro_picture_more),
contentDescription = "",
)
}
} }
} }
} }
} }
} }
} }
PullRefreshIndicator( PullRefreshIndicator(
FavouriteListViewModel.isLoading, FavouriteListViewModel.isLoading,
state, state,

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -19,9 +20,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -37,6 +41,8 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -67,7 +73,47 @@ fun FollowerListScreen(userId: Int) {
) { ) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
} }
if (users.itemCount == 0) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if (users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -80,24 +126,32 @@ fun FollowerListScreen(userId: Int) {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.qst_fs_qs_img), else R.mipmap.invalid_name_8),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp, 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注哦", text = stringResource(R.string.follower_empty_title),
color = appColors.text, color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝~", text = stringResource(R.string.follower_empty_subtitle),
color = appColors.text, color = appColors.text,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -24,6 +25,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
@@ -37,7 +40,9 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.FollowButton import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
/** /**
* 关注消息列表 * 关注消息列表
@@ -54,19 +59,51 @@ fun FollowerNoticeScreen() {
val model = FollowerNoticeViewModel val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems() var followers = dataFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
.background(color = AppColors.background)
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.reload() model.reload()
model.updateNotice() model.updateNotice()
} }
if (followers.itemCount == 0) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.reload(force = true)
}
)
}
}
} else if (followers.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -79,24 +116,33 @@ fun FollowerNoticeScreen() {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img id = if(AppState.darkMode) R.mipmap.frame_4
else R.mipmap.qst_fs_qs_img), else R.mipmap.invalid_name_8),
contentDescription = "No Followers", contentDescription = "No Followers",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "还没有人关注哦", text = stringResource(R.string.follower_empty_title),
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "去发布动态,吸引更多粉丝~", text = stringResource(R.string.follower_empty_subtitle),
color = AppColors.text, color = AppColors.text,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
} }

View File

@@ -20,9 +20,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -37,7 +40,9 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -69,7 +74,48 @@ fun FollowingListScreen(userId: Int) {
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false) NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
} }
if(users.itemCount == 0) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if(users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -82,24 +128,32 @@ fun FollowingListScreen(userId: Int) {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my id = if(AppState.darkMode) R.mipmap.frame_3
else R.mipmap.qst_gz_qs_img_my), else R.mipmap.invalid_name_9),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp, 153.dp)
) )
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
androidx.compose.material.Text( androidx.compose.material.Text(
text = "没有关注任何灵魂", text = stringResource(R.string.following_empty_title),
color = appColors.text, color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text( androidx.compose.material.Text(
text = "探索一下,总有一个你想靠近的光点 ✨", text = stringResource(R.string.following_empty_subtitle),
color = appColors.secondaryText, color = appColors.secondaryText,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
} }

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.group package com.aiosman.ravenow.ui.group
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -8,6 +9,11 @@ import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ChatState import com.aiosman.ravenow.ChatState
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.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.parseErrorResponse
import com.aiosman.ravenow.entity.ChatNotification import com.aiosman.ravenow.entity.ChatNotification
import com.aiosman.ravenow.entity.GroupInfo import com.aiosman.ravenow.entity.GroupInfo
import com.aiosman.ravenow.entity.GroupMember import com.aiosman.ravenow.entity.GroupMember
@@ -22,9 +28,36 @@ class GroupChatInfoViewModel(
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var error by mutableStateOf<String?>(null) var error by mutableStateOf<String?>(null)
var chatNotification by mutableStateOf<ChatNotification?>(null) var chatNotification by mutableStateOf<ChatNotification?>(null)
var isAddingMemory by mutableStateOf(false)
var addMemoryError by mutableStateOf<String?>(null)
var addMemorySuccess by mutableStateOf(false)
val notificationStrategy get() = chatNotification?.strategy ?: "default" val notificationStrategy get() = chatNotification?.strategy ?: "default"
// 记忆管理相关状态
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)
init { init {
loadGroupInfo() loadGroupInfo()
loadPromptOpenId()
}
/**
* 获取群聊中智能体的 OpenID
*/
private fun loadPromptOpenId() {
viewModelScope.launch {
try {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
promptOpenId = prompts?.firstOrNull()?.openId
} catch (e: Exception) {
Log.e("GroupChatInfoViewModel", "获取智能体OpenID失败: ${e.message}", e)
}
}
} }
suspend fun updateNotificationStrategy(strategy: String) { suspend fun updateNotificationStrategy(strategy: String) {
val result = ChatState.updateChatNotification(groupId.hashCode(), strategy) val result = ChatState.updateChatNotification(groupId.hashCode(), strategy)
@@ -71,4 +104,218 @@ class GroupChatInfoViewModel(
} }
} }
} }
/**
* 添加群记忆
* @param memoryText 记忆内容
* @param promptOpenId 智能体的 OpenID可选如果不提供则从群聊信息中获取
*/
fun addGroupMemory(memoryText: String, promptOpenId: String? = null) {
viewModelScope.launch {
try {
isAddingMemory = true
addMemoryError = null
addMemorySuccess = false
// 如果没有提供 promptOpenId需要先获取群聊的智能体信息
val openId = promptOpenId ?: run {
// 通过 createGroupChatAi 接口获取群聊详细信息(包含 prompts
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
if (prompts.isNullOrEmpty()) {
throw Exception("群聊中没有找到智能体,无法添加记忆")
}
// 使用第一个智能体的 openId
prompts.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
}
if (openId.isBlank()) {
throw Exception("智能体ID不能为空")
}
// 创建智能体规则(群记忆)
val requestBody = CreateAgentRuleRequestBody(
rule = memoryText,
openId = openId
)
val response = ApiClient.api.createAgentRule(requestBody)
if (response.isSuccessful) {
addMemorySuccess = true
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "添加群记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
addMemoryError = e.message ?: "添加群记忆失败"
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
} finally {
isAddingMemory = false
}
}
}
/**
* 获取记忆配额信息
*/
fun loadMemoryQuota(openId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
val quotaResponse = ApiClient.api.getAgentRuleQuota(fetchedOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
} else {
val quotaResponse = ApiClient.api.getAgentRuleQuota(targetOpenId)
if (quotaResponse.isSuccessful) {
memoryQuota = quotaResponse.body()?.data
} else {
throw Exception("获取配额信息失败: ${quotaResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取配额信息失败"
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 获取记忆列表
*/
fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val targetOpenId = openId ?: promptOpenId
if (targetOpenId.isNullOrBlank()) {
// 如果还没有获取到 openId先获取
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
val groupChatResponse = response.body()?.data
val prompts = groupChatResponse?.prompts
val fetchedOpenId = prompts?.firstOrNull()?.openId
?: throw Exception("无法获取智能体信息")
promptOpenId = fetchedOpenId
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.getAgentRuleList(targetOpenId, page = page, pageSize = pageSize)
if (listResponse.isSuccessful) {
memoryList = listResponse.body()?.data?.list ?: emptyList()
} else {
throw Exception("获取记忆列表失败: ${listResponse.code()}")
}
}
} catch (e: Exception) {
memoryError = e.message ?: "获取记忆列表失败"
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 删除记忆
*/
fun deleteMemory(ruleId: Int) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val response = ApiClient.api.deleteAgentRule(ruleId)
if (response.isSuccessful) {
// 刷新记忆列表和配额
promptOpenId?.let { openId ->
loadMemoryQuota(openId)
loadMemoryList(openId)
}
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "删除记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "删除记忆失败"
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
/**
* 更新记忆
*/
fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) {
viewModelScope.launch {
try {
isLoadingMemory = true
memoryError = null
val openId = targetOpenId ?: promptOpenId
?: throw Exception("无法获取智能体ID")
val requestBody = UpdateAgentRuleRequestBody(
id = ruleId,
rule = newRuleText,
openId = openId
)
val response = ApiClient.api.updateAgentRule(requestBody)
if (response.isSuccessful) {
// 刷新记忆列表和配额
loadMemoryQuota(openId)
loadMemoryList(openId)
} else {
val errorResponse = parseErrorResponse(response.errorBody())
val errorMessage = errorResponse?.toServiceException()?.message
?: "更新记忆失败: ${response.code()}"
throw Exception(errorMessage)
}
} catch (e: Exception) {
memoryError = e.message ?: "更新记忆失败"
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
} finally {
isLoadingMemory = false
}
}
}
} }

View File

@@ -0,0 +1,625 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import androidx.compose.ui.graphics.Brush
@Composable
fun GroupMemoryManageContent(
groupId: String,
viewModel: GroupChatInfoViewModel,
onAddMemoryClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.95f
val context = LocalContext.current
// 编辑记忆的状态 - 存储正在编辑的记忆ID
var editingMemoryId by remember { mutableStateOf<Int?>(null) }
// 加载配额和列表数据
LaunchedEffect(Unit) {
viewModel.loadMemoryQuota()
viewModel.loadMemoryList()
}
val quota = viewModel.memoryQuota
val memoryList = viewModel.memoryList
val isLoading = viewModel.isLoadingMemory
Column(
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight)
.background(Color(0xFFFAF9FB))
) {
// 顶部栏:返回按钮 + 标题 + 加号按钮
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 16.dp)
) {
// 中间标题 - 绝对居中,不受其他组件影响
Text(
text = "记忆管理",
style = TextStyle(
color = Color.Black,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold
),
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
// 左侧返回按钮
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.clip(RoundedCornerShape(296.dp))
.background(Color.White) // 浅灰色背景
.noRippleClickable { onDismiss() }
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.rider_pro_back_icon),
contentDescription = "返回",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color.Black)
)
Text(
text = "返回",
style = TextStyle(
color = Color.Black,
fontSize = 15.sp,
fontWeight = FontWeight.Normal
)
)
}
// 右侧圆形加号按钮
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.size(32.dp)
.clip(CircleShape)
.background(Color.White)
.noRippleClickable { onAddMemoryClick() },
contentAlignment = Alignment.Center
) {
Text(
text = "+",
style = TextStyle(
color = Color.Black,
fontSize = 20.sp,
fontWeight = FontWeight.Medium
)
)
}
}
// 浅黄色提示栏 - 显示真实的配额数据
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFFBF8EF))
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.purchasedCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"${quota?.currentCount ?: 0}",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
Spacer(Modifier.width(3.dp))
Text(
"50",
style = TextStyle(color = Color(0xFFFF8D28), fontSize = 13.sp)
)
}
}
// 记忆列表或空状态
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
if (isLoading) {
// 加载中状态
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(40.dp),
color = Color(0xFFFF8D28)
)
}
} else if (memoryList.isNotEmpty()) {
// 显示记忆列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(memoryList) { memory ->
MemoryItem(
memory = memory,
isEditing = editingMemoryId == memory.id,
onEdit = {
editingMemoryId = memory.id
},
onCancel = {
editingMemoryId = null
},
onSave = { newText ->
viewModel.updateMemory(memory.id, newText)
editingMemoryId = null
},
onDelete = {
viewModel.deleteMemory(memory.id)
}
)
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.mipmap.group),
contentDescription = "暂无记忆",
modifier = Modifier
.height(150.dp).width(180.dp)
)
Spacer(Modifier.height(10.dp))
Text(
text = "暂无记忆",
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
)
Spacer(Modifier.height(6.dp))
Text(
text = "点击上方按钮添加群记忆",
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
)
}
}
}
}
}
/**
* 编辑记忆对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditGroupMemoryDialog(
memory: com.aiosman.ravenow.data.api.AgentRule,
viewModel: GroupChatInfoViewModel,
onDismiss: () -> Unit,
onUpdateMemory: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color(0xFFFAF9FB),
dragHandle = {
Box(
modifier = Modifier
.width(36.dp)
.height(5.dp)
.padding(top = 5.dp)
.background(
Color(0xFFCCCCCC),
RoundedCornerShape(100.dp)
)
)
},
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onDismiss() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onUpdateMemory(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
}
}
/**
* 记忆项组件
*/
@Composable
fun MemoryItem(
memory: com.aiosman.ravenow.data.api.AgentRule,
isEditing: Boolean = false,
onEdit: () -> Unit = {},
onCancel: () -> Unit = {},
onSave: (String) -> Unit = {},
onDelete: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var memoryText by remember { mutableStateOf(memory.rule) }
val maxLength = 500
// 渐变边框颜色
val gradientColors = listOf(
Color(0xFF7C45ED),
Color(0xFF7C57EE),
Color(0xFF7BD8F8)
)
val gradientBrush = Brush.horizontalGradient(colors = gradientColors)
// 当进入编辑模式时,重置文本
LaunchedEffect(isEditing) {
if (isEditing) {
memoryText = memory.rule
}
}
if (isEditing) {
// 编辑模式:显示编辑界面
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 文本输入框 - 带渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.height(88.dp)
) {
// 渐变边框层
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(brush = gradientBrush)
)
// 内容层 - 白色背景通过padding形成边框效果
Box(
modifier = Modifier
.fillMaxSize()
.padding(1.dp)
.clip(RoundedCornerShape(15.dp))
.background(Color.White)
.padding(12.dp),
contentAlignment = Alignment.TopStart
) {
BasicTextField(
value = memoryText,
onValueChange = { newText ->
if (newText.length <= maxLength) {
memoryText = newText
}
},
cursorBrush = SolidColor(Color.Black),
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 13.sp,
color = Color.Black,
lineHeight = 18.sp
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
innerTextField()
}
}
)
}
}
// 按钮行:取消和保存
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0x147C7480))
.noRippleClickable { onCancel() },
contentAlignment = Alignment.Center
) {
Text(
text = "取消",
style = TextStyle(
fontSize = 15.sp,
color = Color.Black
)
)
}
// 保存按钮
Box(
modifier = Modifier
.weight(1f)
.height(34.dp)
.clip(RoundedCornerShape(653.8.dp))
.background(Color(0xFF110C13))
.noRippleClickable {
if (memoryText.isNotBlank() && memoryText != memory.rule) {
onSave(memoryText)
Toast.makeText(context, "记忆更新成功", Toast.LENGTH_SHORT).show()
}
},
contentAlignment = Alignment.Center
) {
Text(
text = "保存",
style = TextStyle(
fontSize = 15.sp,
color = Color.White
)
)
}
}
}
} else {
// 显示模式:显示记忆内容
// 格式化日期:从 "2025-10-20T10:30:00Z" 格式转换为 "2025年10月20日"
val formattedDate = try {
if (memory.createdAt.length >= 10) {
val dateStr = memory.createdAt.substring(0, 10)
val parts = dateStr.split("-")
if (parts.size == 3) {
"${parts[0]}${parts[1].toInt()}${parts[2].toInt()}"
} else {
dateStr
}
} else {
memory.createdAt
}
} catch (e: Exception) {
if (memory.createdAt.length >= 10) memory.createdAt.substring(0, 10) else memory.createdAt
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.padding(horizontal = 12.dp, vertical = 16.dp)
) {
// 主文本 - 顶部
Text(
text = memory.rule,
style = TextStyle(
color = Color.Black,
fontSize = 13.sp,
lineHeight = 18.sp
),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 底部行:日期 + 编辑删除按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom
) {
// 日期文本 - 左侧
Text(
text = formattedDate,
style = TextStyle(
color = Color(0x993C3C43),
fontSize = 11.sp
)
)
// 编辑和删除图标 - 右侧
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 编辑图标
Image(
painter = painterResource(R.mipmap.icons_infor_edit),
contentDescription = "编辑",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onEdit() },
colorFilter = ColorFilter.tint(Color.Black)
)
Image(
painter = painterResource(R.mipmap.iconsdelete),
contentDescription = "删除",
modifier = Modifier
.size(20.dp)
.noRippleClickable { onDelete() },
colorFilter = ColorFilter.tint(Color(0xFFEE2A33))
)
}
}
}
}
}

View File

@@ -43,7 +43,7 @@ fun CreateBottomSheet(
onMomentClick: () -> Unit onMomentClick: () -> Unit
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
//水平效果呈现镜像排列
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
@@ -58,42 +58,59 @@ fun CreateBottomSheet(
.padding(top = 24.dp, bottom = 24.dp), .padding(top = 24.dp, bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 标题 Row(
Text( modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.create_title), verticalAlignment = Alignment.CenterVertically
fontSize = 18.sp, ) {
fontWeight = FontWeight.Bold, Image(
color = appColors.text, painter = painterResource(R.mipmap.h_cj_rw_icon),
modifier = Modifier.padding(bottom = 32.dp) contentDescription = null,
) modifier = Modifier
.padding(start = 16.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.create_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
modifier = Modifier
.padding(end = 3.dp)
)
Image(
painter = painterResource(R.mipmap.h_cj_x_img),
contentDescription = null,
modifier = Modifier
.padding(end = 18.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(appColors.text)
)
}
Spacer(modifier = Modifier.height(30.dp))
// 三个创建选项 // 三个创建选项
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
// 群聊选项
CreateOption(
icon = R.drawable.ic_create_group_chat,
label = stringResource(R.string.create_group_chat_option),
onClick = onGroupChatClick
)
// 动态选项 // 动态选项
CreateOption( CreateOption(
icon = R.drawable.ic_create_monent, icon = R.drawable.ic_create_monent,
label = stringResource(R.string.create_moment), label = stringResource(R.string.create_moment),
onClick = onMomentClick onClick = onMomentClick
) )
// 群聊选项
CreateOption(
icon = R.mipmap.icons_circle_camera,
label = stringResource(R.string.create_group_chat_option),
onClick = onGroupChatClick
)
// AI选项 // AI选项
CreateOption( CreateOption(
icon = R.drawable.ic_create_ai, icon = R.mipmap.icons_circle_ai,
label = stringResource(R.string.create_ai), label = stringResource(R.string.create_ai),
onClick = onAiClick onClick = onAiClick
) )
} }
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))

View File

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

View File

@@ -16,14 +16,14 @@ sealed class NavigationItem(
) { ) {
data object Home : NavigationItem("Home", data object Home : NavigationItem("Home",
icon = { R.drawable.rider_pro_nav_home }, icon = { R.mipmap.bars_x_buttons_home_n_copy },
selectedIcon = { R.mipmap.rider_pro_nav_home_hl }, selectedIcon = { R.mipmap.bars_x_buttons_home_n_copy_2 },
label = { stringResource(R.string.main_home) } label = { stringResource(R.string.main_ai) }
) )
data object Ai : NavigationItem("Ai", data object Ai : NavigationItem("Ai",
icon = { R.mipmap.bars_x_buttons_discover_bold}, icon = { R.mipmap.bars_x_buttons_discover_bold},
selectedIcon = { R.mipmap.dynamic_hl }, selectedIcon = { R.mipmap.bars_x_buttons_discover_fill },
label = { stringResource(R.string.index_dynamic) } label = { stringResource(R.string.index_dynamic) }
) )
// data object Ai : NavigationItem("Ai", // data object Ai : NavigationItem("Ai",
@@ -40,13 +40,13 @@ sealed class NavigationItem(
data object Notification : NavigationItem("Notification", data object Notification : NavigationItem("Notification",
icon = { R.drawable.rider_pro_nav_notification }, icon = { R.drawable.rider_pro_nav_notification },
selectedIcon = { R.mipmap.rider_pro_nav_message_hl }, selectedIcon = { R.mipmap.bars_x_buttons_chat_s },
label = { stringResource(R.string.main_message) } label = { stringResource(R.string.main_message) }
) )
data object Profile : NavigationItem("Profile", data object Profile : NavigationItem("Profile",
icon = { R.drawable.rider_pro_nav_profile }, icon = { R.drawable.rider_pro_nav_profile },
selectedIcon = { R.mipmap.rider_pro_nav_profile_hl }, selectedIcon = { R.mipmap.bars_x_buttons_user_s },
label = { stringResource(R.string.main_profile) } label = { stringResource(R.string.main_profile) }
) )

View File

@@ -6,7 +6,10 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CategoryTemplate
import com.aiosman.ravenow.data.api.RaveNowAPI import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
@@ -14,6 +17,37 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.util.Log
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.api.CreateGroupChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.data.api.ApiErrorResponse
import com.google.gson.Gson
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.launch
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
/**
* 缓存数据结构用于存储每个分类的Agent列表
*/
data class AgentCacheData(
val items: List<AgentItem>,
val currentPage: Int,
val hasMoreData: Boolean
)
data class ChatRoom(
val id: Int,
val name: String,
val avatar: String = "",
val banner: String = "",
val memberCount: Int
)
object AgentViewModel: ViewModel() { object AgentViewModel: ViewModel() {
@@ -22,10 +56,17 @@ object AgentViewModel: ViewModel() {
var agentItems by mutableStateOf<List<AgentItem>>(emptyList()) var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
private set private set
var categories by mutableStateOf<List<CategoryItem>>(emptyList())
private set
var errorMessage by mutableStateOf<String?>(null) var errorMessage by mutableStateOf<String?>(null)
private set private set
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
private set
var rooms by mutableStateOf<List<Room>>(emptyList())
private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
@@ -33,31 +74,242 @@ object AgentViewModel: ViewModel() {
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
private set private set
// 分页相关状态
var isLoadingMore by mutableStateOf(false)
private set
var currentPage by mutableStateOf(1)
private set
var hasMoreData by mutableStateOf(true)
private set
var isJoiningRoom by mutableStateOf(false)
private set
private val pageSize = 20
private var currentCategoryId: Int? = null
// 缓存使用分类ID作为keynull表示推荐列表
private val agentCache = mutableMapOf<Int?, AgentCacheData>()
init { init {
loadAgentData() loadAgentData()
loadCategories()
loadChatRooms()
} }
private fun loadAgentData() { private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false, forceRefresh: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
isLoading = true // 如果不是强制刷新且不是加载更多,检查缓存
if (!forceRefresh && !isLoadMore) {
val cached = agentCache[categoryId]
if (cached != null && cached.items.isNotEmpty()) {
// 使用缓存数据
agentItems = cached.items
currentPage = cached.currentPage
hasMoreData = cached.hasMoreData
currentCategoryId = categoryId
println("使用缓存数据分类ID: $categoryId, 数据数量: ${cached.items.size}")
return@launch
}
}
if (isLoadMore) {
isLoadingMore = true
} else {
isLoading = true
// 重置分页状态
currentPage = 1
hasMoreData = true
currentCategoryId = categoryId
}
errorMessage = null errorMessage = null
try { try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1) val response = if (categoryId != null) {
// 根据分类ID获取智能体
apiClient.getAgent(
page = page,
pageSize = pageSize,
withWorkflow = 1,
categoryIds = listOf(categoryId),
random = 1
)
} else {
// 获取推荐智能体使用random=1
apiClient.getAgent(
page = page,
pageSize = pageSize,
withWorkflow = 1,
categoryIds = null,
random = 1
)
}
if (response.isSuccessful) { if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList() val responseData = response.body()?.data
agentItems = agents.map { agent -> val agents = responseData?.list ?: emptyList<Agent>()
val newAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent) AgentItem.fromAgent(agent)
} }
if (isLoadMore) {
// 加载更多:追加到现有列表
agentItems = agentItems + newAgentItems
currentPage = page
} else {
// 首次加载或刷新:替换整个列表
agentItems = newAgentItems
currentPage = 1
}
// 检查是否还有更多数据
hasMoreData = agents.size >= pageSize
// 更新缓存
agentCache[categoryId] = AgentCacheData(
items = agentItems,
currentPage = currentPage,
hasMoreData = hasMoreData
)
println("更新缓存分类ID: $categoryId, 数据数量: ${agentItems.size}")
} else { } else {
errorMessage = "获取Agent数据失败: ${response.code()}" errorMessage = "获取Agent数据失败: ${response.code()}"
} }
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}" errorMessage = "网络请求失败: ${e.message}"
} finally { } finally {
isLoading = false if (isLoadMore) {
isLoadingMore = false
} else {
isLoading = false
}
} }
} }
} }
private fun loadChatRooms() {
viewModelScope.launch {
try {
val response = apiClient.getRooms(
page = 1,
pageSize = 20,
isRecommended = 1,
random = 1
)
if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList()
val targetCount = (allRooms.size / 2) * 2
rooms = allRooms.take(targetCount)
// 转换为ChatRoom格式用于兼容现有UI
chatRooms = rooms.map { room ->
ChatRoom(
id = room.id,
name = room.name,
avatar = room.avatar,
banner = ConstVars.BASE_SERVER + "/api/v1/outside/" + room.recommendBanner + "?token=${AppStore.token}",
memberCount = room.userCount
)
}
} else {
}
} catch (e: Exception) {
// 如果网络请求失败,使用默认数据
}
}
}
private fun loadCategories() {
viewModelScope.launch {
// 如果分类已经加载,不重复请求
if (categories.isNotEmpty()) {
println("使用已缓存的分类数据,数量: ${categories.size}")
return@launch
}
try {
// 获取完整的语言标记(如 "zh-CN"
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,
lang = 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)
}
println("成功处理并映射了 ${categories.size} 个分类")
} else {
errorMessage = "获取分类数据失败: ${response.code()}"
println("获取分类数据失败: ${response.code()}")
}
} catch (e: Exception) {
errorMessage = "获取分类数据失败: ${e.message}"
println("获取分类数据异常: ${e.message}")
e.printStackTrace()
}
}
}
/**
* 将完整的语言标记转换为后端支持的语言代码
* 后端仅支持: 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)
}
fun loadAllAgents() {
loadAgentData()
}
/**
* 加载更多Agent数据
*/
fun loadMoreAgents() {
// 检查是否正在加载或没有更多数据
if (isLoadingMore || !hasMoreData) {
return
}
val nextPage = currentPage + 1
loadAgentData(
categoryId = currentCategoryId,
page = nextPage,
isLoadMore = true
)
}
fun createSingleChat( fun createSingleChat(
openId: String, openId: String,
) { ) {
@@ -96,10 +348,12 @@ object AgentViewModel: ViewModel() {
} }
/** /**
* 刷新推荐Agent数据 * 刷新当前分类的Agent数据(强制刷新,清除缓存)
*/ */
fun refreshAgentData() { fun refreshAgentData() {
loadAgentData() // 清除当前分类的缓存
agentCache.remove(currentCategoryId)
loadAgentData(categoryId = currentCategoryId, forceRefresh = true)
} }
/** /**
@@ -111,14 +365,115 @@ object AgentViewModel: ViewModel() {
} }
} }
/**
* 加入房间
*/
fun joinRoom(
id: Int,
name: String,
avatar: String,
context: Context,
navController: NavHostController,
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
// 防止重复点击
if (isJoiningRoom) return
viewModelScope.launch {
try {
isJoiningRoom = true
val response = apiClient.joinRoom(JoinGroupChatRequestBody(roomId = id))
if (response.isSuccessful) {
// 打开房间
val openRoomResponse = apiClient.createGroupChatAi(
roomId = id
)
if (openRoomResponse.isSuccessful){
val respData = openRoomResponse.body()
respData?.let {
viewModelScope.launch {
try {
// 群聊直接使用群ID进行导航
navController.navigateToGroupChat(
id = respData.data.trtcRoomId,
name = name,
avatar = avatar
)
} catch (e: Exception) {
onError("加入房间失败")
e.printStackTrace()
}
}
}
}
onSuccess()
} else {
// 处理错误响应
try {
val errorBody = response.errorBody()?.string()
if (errorBody != null) {
val gson = Gson()
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
// 在主线程显示 Toast
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
onError(errorResponse.error)
} else {
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
} catch (parseException: Exception) {
// 如果解析错误响应失败,显示默认错误信息
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
}
} catch (e: Exception) {
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
onError("网络请求失败:${e.message}")
} finally {
isJoiningRoom = false
}
}
}
/** /**
* 重置ViewModel状态用于登出或切换账号时清理数据 * 重置ViewModel状态用于登出或切换账号时清理数据
*/ */
fun ResetModel() { fun ResetModel() {
agentItems = emptyList() agentItems = emptyList()
categories = emptyList()
errorMessage = null errorMessage = null
isRefreshing = false isRefreshing = false
isLoading = false isLoading = false
isLoadingMore = false
currentPage = 1
hasMoreData = true
currentCategoryId = null
// 清空缓存
agentCache.clear()
} }
} }
data class CategoryItem(
val id: Int,
val name: String,
val description: String,
val avatar: String,
val promptCount: Int?
) {
companion object {
fun fromCategoryTemplate(template: CategoryTemplate, lang: String): CategoryItem {
return CategoryItem(
id = template.id,
name = template.getLocalizedName(lang),
description = template.getLocalizedDescription(lang),
avatar = "${ApiClient.BASE_API_URL}${template.avatar}",
promptCount = template.promptCount
)
}
}
}

View File

@@ -78,7 +78,7 @@ import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.ui.index.tabs.message.tab.AllChatListScreen
/** /**
* 消息列表界面 * 消息列表界面
@@ -95,7 +95,7 @@ fun NotificationsScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val context = LocalContext.current val context = LocalContext.current
var pagerState = rememberPagerState (pageCount = { 3 }) var pagerState = rememberPagerState (pageCount = { 4 })
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
@@ -177,7 +177,7 @@ fun NotificationsScreen() {
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.noRippleClickable { .noRippleClickable {
// TODO: 实现通知功能 navController.navigate(NavigationRoute.NotificationScreen.route)
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
@@ -324,7 +324,7 @@ fun NotificationsScreen() {
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_ai), text = stringResource(R.string.chat_all),
isSelected = pagerState.currentPage == 0, isSelected = pagerState.currentPage == 0,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
@@ -335,6 +335,38 @@ fun NotificationsScreen() {
} }
) )
// 全部未读消息红点
val totalUnreadCount = AgentChatListViewModel.totalUnreadCount +
GroupChatListViewModel.totalUnreadCount +
FriendChatListViewModel.totalUnreadCount
if (totalUnreadCount > 0) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = Color(0xFFFF3B30),
shape = CircleShape
)
.align(Alignment.TopEnd)
.offset(x = 8.dp, y = (-4).dp)
)
}
}
TabSpacer()
Box {
TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
// 智能体未读消息红点 // 智能体未读消息红点
if (AgentChatListViewModel.totalUnreadCount > 0) { if (AgentChatListViewModel.totalUnreadCount > 0) {
Box( Box(
@@ -353,11 +385,11 @@ fun NotificationsScreen() {
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_group), text = stringResource(R.string.chat_group),
isSelected = pagerState.currentPage == 1, isSelected = pagerState.currentPage == 2,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(1) pagerState.animateScrollToPage(2)
} }
} }
} }
@@ -378,14 +410,15 @@ fun NotificationsScreen() {
} }
} }
TabSpacer() TabSpacer()
Box { Box {
TabItem( TabItem(
text = stringResource(R.string.chat_friend), text = stringResource(R.string.chat_friend),
isSelected = pagerState.currentPage == 2, isSelected = pagerState.currentPage == 3,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(2) pagerState.animateScrollToPage(3)
} }
} }
} }
@@ -414,14 +447,17 @@ fun NotificationsScreen() {
) { ) {
when (it) { when (it) {
0 -> { 0 -> {
AllChatListScreen()
}
1 -> {
AgentChatListScreen() AgentChatListScreen()
} }
1 -> { 2 -> {
GroupChatListScreen() GroupChatListScreen()
} }
2 -> { 3 -> {
FriendChatListScreen() FriendChatListScreen()
} }
} }

View File

@@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -91,29 +92,77 @@ fun AgentChatListScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img id = if(AppState.darkMode) R.mipmap.juhao_dark
else R.mipmap.qs_znt_qs_img), else R.mipmap.invalid_name_5),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.agent_chat_empty_title), text = stringResource(R.string.agent_chat_empty_title),
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
) textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(R.string.agent_chat_empty_subtitle), text = stringResource(R.string.agent_chat_empty_subtitle),
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 14.sp fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
else {
Spacer(modifier = Modifier.height(39.dp))
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),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
AgentChatListViewModel.refreshPager(context = context)
}
)
}
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@@ -0,0 +1,406 @@
package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily
data class CombinedConversation(
val type: String, // "agent", "group", or "friend"
val agentConversation: AgentConversation? = null,
val groupConversation: GroupConversation? = null,
val friendConversation: FriendConversation? = null
) {
val id: String
get() = when (type) {
"agent" -> "agent_${agentConversation?.id ?: 0}"
"group" -> "group_${groupConversation?.id ?: 0}"
"friend" -> "friend_${friendConversation?.id ?: 0}"
else -> ""
}
val avatar: String
get() = when (type) {
"agent" -> agentConversation?.avatar ?: ""
"group" -> groupConversation?.avatar ?: ""
"friend" -> friendConversation?.avatar ?: ""
else -> ""
}
val name: String
get() = when (type) {
"agent" -> agentConversation?.nickname ?: ""
"group" -> groupConversation?.groupName ?: ""
"friend" -> friendConversation?.nickname ?: ""
else -> ""
}
val lastMessageTime: String
get() = when (type) {
"agent" -> agentConversation?.lastMessageTime ?: ""
"group" -> groupConversation?.lastMessageTime ?: ""
"friend" -> friendConversation?.lastMessageTime ?: ""
else -> ""
}
val displayText: String
get() = when (type) {
"agent" -> agentConversation?.displayText ?: ""
"group" -> groupConversation?.displayText ?: ""
"friend" -> friendConversation?.displayText ?: ""
else -> ""
}
val unreadCount: Int
get() = when (type) {
"agent" -> agentConversation?.unreadCount ?: 0
"group" -> groupConversation?.unreadCount ?: 0
"friend" -> friendConversation?.unreadCount ?: 0
else -> 0
}
val isSelf: Boolean
get() = when (type) {
"agent" -> agentConversation?.isSelf ?: false
"group" -> groupConversation?.isSelf ?: false
"friend" -> friendConversation?.isSelf ?: false
else -> false
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AllChatListScreen() {
val context = LocalContext.current
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
var allConversations by remember { mutableStateOf<List<CombinedConversation>>(emptyList()) }
var refreshing by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
val state = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
refreshing = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
refreshing = false
},
onError = { errorMsg ->
error = errorMsg
refreshing = false
}
)
}
)
LaunchedEffect(Unit) {
isLoading = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
isLoading = false
},
onError = { errorMsg ->
error = errorMsg
isLoading = false
}
)
}
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state)
) {
if (allConversations.isEmpty() && !isLoading) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.invalid_name_2),
contentDescription = "null data",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
)
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_empty_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
} else {
Spacer(modifier = Modifier.height(39.dp))
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),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
isLoading = true
refreshAllData(context,
onSuccess = { conversations ->
allConversations = conversations
isLoading = false
},
onError = { errorMsg ->
error = errorMsg
isLoading = false
}
)
}
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(
items = allConversations,
key = { _, item -> item.id }
) { index, item ->
when (item.type) {
"agent" -> {
item.agentConversation?.let { agent ->
AgentChatItem(
conversation = agent,
onUserAvatarClick = { conv ->
AgentChatListViewModel.goToUserDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
AgentChatListViewModel.createSingleChat(conv.trtcUserId)
AgentChatListViewModel.goToChatAi(conv.trtcUserId, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
"group" -> {
item.groupConversation?.let { group ->
GroupChatItem(
conversation = group,
onGroupAvatarClick = { conv ->
GroupChatListViewModel.goToGroupDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
GroupChatListViewModel.goToChat(conv, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
"friend" -> {
item.friendConversation?.let { friend ->
FriendChatItem(
conversation = friend,
onUserAvatarClick = { conv ->
FriendChatListViewModel.goToUserDetail(conv, navController)
},
onChatClick = { conv ->
if (NetworkUtils.isNetworkAvailable(context)) {
FriendChatListViewModel.goToChat(conv, navController)
} else {
Toast.makeText(context, "网络连接异常,请检查网络设置", Toast.LENGTH_SHORT).show()
}
}
)
}
}
}
// 分隔线
// if (index < allConversations.size - 1) {
// HorizontalDivider(
// modifier = Modifier.padding(horizontal = 24.dp),
// color = AppColors.divider
// )
// }
}
if (isLoading && allConversations.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main
)
}
}
}
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
error?.let { errorMsg ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = errorMsg,
color = AppColors.error,
fontSize = 14.sp
)
}
}
}
}
fun refreshAllData(
context: android.content.Context,
onSuccess: (List<CombinedConversation>) -> Unit,
onError: (String) -> Unit
) {
try {
// 同时刷新所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
val combinedList = mutableListOf<CombinedConversation>()
AgentChatListViewModel.agentChatList.forEach { agent ->
combinedList.add(CombinedConversation(type = "agent", agentConversation = agent))
}
GroupChatListViewModel.groupChatList.forEach { group ->
combinedList.add(CombinedConversation(type = "group", groupConversation = group))
}
FriendChatListViewModel.friendChatList.forEach { friend ->
val isDuplicate = combinedList.any {//判断重复
it.type == "agent" && it.agentConversation?.trtcUserId == friend.trtcUserId
}
if (!isDuplicate) {
combinedList.add(CombinedConversation(type = "friend", friendConversation = friend))
}
}
// 按最后消息时间排序
val sortedList = combinedList.sortedByDescending {
it.lastMessageTime
}
onSuccess(sortedList)
} catch (e: Exception) {
onError("刷新数据失败: ${e.message}")
}
}

View File

@@ -33,8 +33,14 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.graphics.Brush
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -73,29 +79,75 @@ fun FriendChatListScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center //verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_py_qs_as_img id = if(AppState.darkMode) R.mipmap.piao_dark
else R.mipmap.qs_py_qs_img), else R.mipmap.invalid_name_2),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.friend_chat_empty_title), text = stringResource(R.string.friend_chat_empty_title),
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
) textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(R.string.friend_chat_empty_subtitle), text = stringResource(R.string.friend_chat_empty_subtitle),
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 14.sp fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
}else {
Spacer(modifier = Modifier.height(39.dp))
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),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -266,4 +318,43 @@ fun FriendChatItem(
} }
} }
} }
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -71,28 +72,75 @@ fun GroupChatListScreen() {
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp)) Spacer(modifier = Modifier.height(39.dp))
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img id = if(AppState.darkMode) R.mipmap.fei_dark
else R.mipmap.qs_ql_qs_img), else R.mipmap.invalid_name_12),
contentDescription = "null data", contentDescription = "null data",
modifier = Modifier modifier = Modifier
.size(181.dp) .size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = stringResource(R.string.group_chat_empty), text = stringResource(R.string.group_chat_empty),
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
) textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(R.string.group_chat_empty_join), text = stringResource(R.string.group_chat_empty_join),
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontSize = 14.sp fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
}else {
Spacer(modifier = Modifier.height(39.dp))
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),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context import android.content.Context
import android.icu.util.Calendar import android.icu.util.Calendar
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -48,7 +49,7 @@ data class GroupConversation(
val lastMessage = Calendar.getInstance().apply { val lastMessage = Calendar.getInstance().apply {
timeInMillis = conversation.latestMsgSendTime timeInMillis = conversation.latestMsgSendTime
} }
// 解析最新消息 // 解析最新消息
val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg) val (displayText, isSelf) = MessageParser.parseLatestMessage(conversation.latestMsg)
@@ -139,7 +140,7 @@ object GroupChatListViewModel : ViewModel() {
android.util.Log.w("GroupChatListViewModel", "OpenIM 未登录,跳过加载群聊列表") android.util.Log.w("GroupChatListViewModel", "OpenIM 未登录,跳过加载群聊列表")
return return
} }
val result = suspendCoroutine { continuation -> val result = suspendCoroutine { continuation ->
// OpenIM 获取所有会话列表 // OpenIM 获取所有会话列表
OpenIMClient.getInstance().conversationManager.getAllConversationList( OpenIMClient.getInstance().conversationManager.getAllConversationList(
@@ -167,10 +168,12 @@ object GroupChatListViewModel : ViewModel() {
} }
fun createGroupChat( fun createGroupChat(
trtcGroupId: String, trtcGroupId: String? = null,
roomId: Int? = null
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId) val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId,roomId = roomId)
Log.d("debug",response.toString())
} }
} }

View File

@@ -17,6 +17,7 @@ import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.ui.follower.FollowerNoticeViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@@ -72,9 +73,12 @@ open class BaseMomentModel :ViewModel(){
} }
suspend fun onAddComment(id: Int) { fun onAddComment(id: Int) {
// val currentPagingData = _momentsFlow.value momentLoader.updateCommentCount(id, +1)
// updateCommentCount(id) }
fun onDeleteComment(id: Int) {
momentLoader.updateCommentCount(id, -1)
} }
@@ -83,6 +87,7 @@ open class BaseMomentModel :ViewModel(){
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) { fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite) momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
} }
suspend fun favoriteMoment(id: Int) { suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true) momentLoader.updateFavoriteCount(id, true)
@@ -118,10 +123,11 @@ open class BaseMomentModel :ViewModel(){
userService.unFollowUser(moment.authorId.toString()) userService.unFollowUser(moment.authorId.toString())
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false)) EventBus.getDefault().post(FollowChangeEvent(moment.authorId, false))
} else { } else {
userService.followUser(moment.authorId.toString()) // 调用 FollowerNoticeViewModel.followUser() 实现与 FollowerNotice.kt 相同的效果
EventBus.getDefault().post(FollowChangeEvent(moment.authorId, true)) // 该方法内部会调用 userService.followUser() 并发布 FollowChangeEvent 事件
FollowerNoticeViewModel.followUser(moment.authorId)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }

View File

@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.index.tabs.moment
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
@@ -45,15 +47,19 @@ import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.Dynamic
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsScreen
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import com.aiosman.ravenow.ui.composables.TabItem import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer import com.aiosman.ravenow.ui.composables.UnderlineTabItem
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.shorts.ShortVideoScreen
/** /**
* 动态列表 * 动态列表
@@ -66,8 +72,8 @@ fun MomentsList() {
val navigationBarPaddings = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下不显示timeline只显示2个tabDynamic、Hot // 游客模式下不显示timeline只显示3个tabExplore、Dynamic、Hot // 根据登录状态设置标签页数量游客模式5个tab非游客模式6个tab
val tabCount = if (AppStore.isGuest) 2 else 3 // val tabCount = if (AppStore.isGuest) 3 else 4 val tabCount = if (AppStore.isGuest) 5 else 6
var pagerState = rememberPagerState { tabCount } var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
Column( Column(
@@ -79,86 +85,28 @@ fun MomentsList() {
), ),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
// 顶部区域:可滚动的标签页 + 搜索按钮
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(44.dp) .height(44.dp)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
// center the tabs horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
//原探索// // 可滚动的标签页行
// Column( Row(
// modifier = Modifier
// .noRippleClickable {
// scope.launch {
// pagerState.animateScrollToPage(0)
// }
// }.padding(start = 16.dp),
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
//
// ) {
// Text(
// text = stringResource(R.string.index_worldwide),
// fontSize = if (pagerState.currentPage == 0)18.sp else 16.sp,
// color = if (pagerState.currentPage == 0) AppColors.text else AppColors.nonActiveText,
// fontWeight = FontWeight.W600)
// Spacer(modifier = Modifier.height(4.dp))
//
// Image(
// painter = painterResource(
// if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
// else R.drawable.tab_indicator_unselected
// ),
// contentDescription = "tab indicator",
// modifier = Modifier
// .width(34.dp)
// .height(4.dp)
// )
//
// }
// Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.moment),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .weight(1f)
) .horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.Start,
Spacer(modifier = Modifier.weight(1f)) verticalAlignment = Alignment.CenterVertically
) {
Image( val tabDebouncer = rememberDebouncer()
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search", // 推荐标签
modifier = Modifier UnderlineTabItem(
.size(24.dp) text = stringResource(R.string.tab_recommend),
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
Spacer(modifier = Modifier.height(23.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
val tabDebouncer = rememberDebouncer()
// 新探索标签
Box {
CustomTabItem(
text = stringResource(R.string.index_worldwide),
isSelected = pagerState.currentPage == 0, isSelected = pagerState.currentPage == 0,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
@@ -168,58 +116,111 @@ fun MomentsList() {
} }
} }
) )
}
TabSpacer()
// 短视频标签
UnderlineTabItem(
text = stringResource(R.string.tab_short_video),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
// 只有非游客用户才显示"关注"tab
if (!AppStore.isGuest) { // 动态标签
Box { UnderlineTabItem(
CustomTabItem( text = stringResource(R.string.moment),
isSelected = pagerState.currentPage == 2,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
}
)
// 只有非游客用户才显示"关注"tab
if (!AppStore.isGuest) {
UnderlineTabItem(
text = stringResource(R.string.index_following), text = stringResource(R.string.index_following),
isSelected = pagerState.currentPage == 1, isSelected = pagerState.currentPage == 3,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(1) pagerState.animateScrollToPage(3)
}
}
}
)
// 热门标签
UnderlineTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 4,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(4)
}
}
}
)
} else {
// 热门标签 (游客模式) - 在游客模式下热门标签对应第3页
UnderlineTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 3,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(3)
} }
} }
} }
) )
} }
TabSpacer()
// 热门标签 // 新闻标签 - 在游客模式下对应第4页非游客模式下对应第5页
Box { val newsPageIndex = if (AppStore.isGuest) 4 else 5
CustomTabItem( UnderlineTabItem(
text = stringResource(R.string.index_hot), text = stringResource(R.string.tab_news),
isSelected = pagerState.currentPage == 2, isSelected = pagerState.currentPage == newsPageIndex,
onClick = { onClick = {
tabDebouncer { tabDebouncer {
scope.launch { scope.launch {
pagerState.animateScrollToPage(2) pagerState.animateScrollToPage(newsPageIndex)
}
} }
} }
) }
} )
} else {
// 热门标签 (游客模式)
Box {
CustomTabItem(
text = stringResource(R.string.index_hot),
isSelected = pagerState.currentPage == 1,
onClick = {
tabDebouncer {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
}
)
}
} }
// 搜索按钮
val lastClickTime = remember { mutableStateOf(0L) }
val clickDelay = 500L
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime.value > clickDelay) {
lastClickTime.value = currentTime
navController.navigate(NavigationRoute.Search.route)
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
} }
HorizontalPager( HorizontalPager(
@@ -228,27 +229,39 @@ fun MomentsList() {
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
) { ) {
if (AppStore.isGuest) { when (it) {
// 游客模式Dynamic(0), Hot(1) 0 -> {
when (it) { // 推荐页面
0 -> { NewsScreen()
Dynamic() }
1 -> {
// 短视频页面
ShortVideoScreen()
}
2 -> {
// 动态页面 - 暂时显示时间线内容
Dynamic()
}
3 -> {
// 关注页面 (仅非游客用户) 或 热门页面 (游客用户)
if (AppStore.isGuest) {
HotMomentsList()
} else {
TimelineMomentsList()
} }
1 -> { }
4 -> {
// 热门页面 (仅非游客用户) 或 新闻页面 (游客用户)
if (AppStore.isGuest) {
NewsScreen()
} else {
HotMomentsList() HotMomentsList()
} }
} }
} else { 5 -> {
// 正常用户Dynamic(0), Timeline(1), Hot(2) // 新闻页面 (仅非游客用户)
when (it) { if (!AppStore.isGuest) {
0 -> { NewsScreen()
Dynamic()
}
1 -> {
TimelineMomentsList()
}
2 -> {
HotMomentsList()
} }
} }
} }

View File

@@ -168,6 +168,7 @@ fun Dynamic() {
) )
} }
} }
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
} }
} }
} }

View File

@@ -369,6 +369,7 @@ fun Explore() {
trtcId = roomItem.trtcId.toString(), trtcId = roomItem.trtcId.toString(),
name = roomItem.title, name = roomItem.title,
avatar = roomItem.avatar, avatar = roomItem.avatar,
context = context,
navController = navController, navController = navController,
onSuccess = { onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show() Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
@@ -523,7 +524,7 @@ fun Explore() {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
/* Image( Image(
painter = painterResource(R.drawable.rider_pro_nav_profile), painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat", contentDescription = "chat",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
@@ -535,7 +536,7 @@ fun Explore() {
fontSize = 12.sp, fontSize = 12.sp,
color = Color.White, color = Color.White,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500 fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)*/ )
} }
// 底部:标题和描述 // 底部:标题和描述
@@ -636,6 +637,7 @@ fun Explore() {
trtcId = bannerItem.trtcId.toString(), trtcId = bannerItem.trtcId.toString(),
name = bannerItem.title, name = bannerItem.title,
avatar = bannerItem.avatar, avatar = bannerItem.avatar,
context = context,
navController = navController, navController = navController,
onSuccess = { onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show() Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()

View File

@@ -17,32 +17,36 @@ import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userServic
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.data.api.ApiErrorResponse
import com.google.gson.Gson
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ExploreViewModel : ViewModel() { class ExploreViewModel : ViewModel() {
private val apiClient: RaveNowAPI = ApiClient.api private val apiClient: RaveNowAPI = ApiClient.api
var bannerItems by mutableStateOf<List<BannerItem>>(emptyList()) var bannerItems by mutableStateOf<List<BannerItem>>(emptyList())
private set private set
var agentItems by mutableStateOf<List<AgentItem>>(emptyList()) var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
private set private set
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
private set private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
var errorMessage by mutableStateOf<String?>(null) var errorMessage by mutableStateOf<String?>(null)
private set private set
init { init {
loadBannerData() loadBannerData()
loadAgentData() loadAgentData()
} }
fun refreshBannerData() { fun refreshBannerData() {
viewModelScope.launch { viewModelScope.launch {
isRefreshing = true isRefreshing = true
@@ -64,7 +68,7 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
fun refreshAgentData() { fun refreshAgentData() {
viewModelScope.launch { viewModelScope.launch {
isRefreshing = true isRefreshing = true
@@ -86,7 +90,7 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
private fun loadBannerData() { private fun loadBannerData() {
viewModelScope.launch { viewModelScope.launch {
isLoading = true isLoading = true
@@ -108,7 +112,7 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
private fun loadAgentData() { private fun loadAgentData() {
viewModelScope.launch { viewModelScope.launch {
isLoading = true isLoading = true
@@ -130,21 +134,24 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
fun createSingleChat( fun createSingleChat(
openId: String, openId: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId)) val response =
ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
} }
} }
fun goToChatAi( fun goToChatAi(
openId: String, openId: String,
navController: NavHostController navController: NavHostController
) { ) {
viewModelScope.launch { viewModelScope.launch {
val profile = userService.getUserProfileByOpenId(openId) val profile = userService.getUserProfileByOpenId(openId)
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id) createGroup2ChatAi(profile.trtcUserId, "ai_group", navController, profile.id)
} }
} }
@@ -152,6 +159,7 @@ class ExploreViewModel : ViewModel() {
trtcId: String, trtcId: String,
name: String, name: String,
avatar: String, avatar: String,
context: Context,
navController: NavHostController, navController: NavHostController,
onSuccess: () -> Unit, onSuccess: () -> Unit,
onError: (String) -> Unit onError: (String) -> Unit
@@ -160,24 +168,45 @@ class ExploreViewModel : ViewModel() {
try { try {
val response = apiClient.joinRoom(JoinGroupChatRequestBody(trtcId = trtcId)) val response = apiClient.joinRoom(JoinGroupChatRequestBody(trtcId = trtcId))
if (response.isSuccessful) { if (response.isSuccessful) {
viewModelScope.launch { viewModelScope.launch {
try { try {
createGroupChat(trtcGroupId = trtcId) createGroupChat(trtcGroupId = trtcId)
// 群聊直接使用群ID进行导航 // 群聊直接使用群ID进行导航
navController.navigateToGroupChat( id = trtcId, navController.navigateToGroupChat(
name = name, id = trtcId,
avatar = avatar) name = name,
} catch (e: Exception) { avatar = avatar
onError("加入房间失败") )
e.printStackTrace() } catch (e: Exception) {
} onError("加入房间失败")
} e.printStackTrace()
onSuccess() }
}
onSuccess()
} else { } else {
onError("加入房间失败") // 处理错误响应
try {
val errorBody = response.errorBody()?.string()
if (errorBody != null) {
val gson = Gson()
val errorResponse = gson.fromJson(errorBody, ApiErrorResponse::class.java)
// 在主线程显示 Toast
Toast.makeText(context, errorResponse.error, Toast.LENGTH_LONG).show()
onError(errorResponse.error)
} else {
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
} catch (parseException: Exception) {
// 如果解析错误响应失败,显示默认错误信息
Toast.makeText(context, "加入房间失败", Toast.LENGTH_SHORT).show()
onError("加入房间失败")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(context, "网络请求失败:${e.message}", Toast.LENGTH_SHORT).show()
onError("网络请求失败:${e.message}") onError("网络请求失败:${e.message}")
} }
} }

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
@@ -13,11 +14,14 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
@@ -39,6 +43,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
@@ -104,8 +109,8 @@ fun DiscoverView() {
val isLoading by model.isLoading.collectAsState() val isLoading by model.isLoading.collectAsState()
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val gridState = rememberLazyGridState() val gridState = rememberLazyStaggeredGridState()
val AppColors = LocalAppTheme.current
// 监听滚动到底部,自动加载更多 // 监听滚动到底部,自动加载更多
LaunchedEffect(gridState, moments.size) { LaunchedEffect(gridState, moments.size) {
snapshotFlow { snapshotFlow {
@@ -124,18 +129,49 @@ fun DiscoverView() {
} }
} }
LazyVerticalGrid( LazyVerticalStaggeredGrid(
columns = GridCells.Fixed(3), columns = StaggeredGridCells.Fixed(2),
state = gridState, state = gridState,
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp), modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
// contentPadding = PaddingValues(8.dp) contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp)
) { ) {
items(moments) { momentItem -> items(moments) { momentItem ->
val debouncer = rememberDebouncer() val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent
// 对于英文和日文,每行字符数会更少,使用更保守的估算
val estimatedCharsPerLine = if (textContent.isNotEmpty()) {
// 检测是否包含非中文字符(英文、日文等)
val hasNonChinese = textContent.any {
val code = it.code
!(code >= 0x4E00 && code <= 0x9FFF) // 不在中文字符范围内
}
if (hasNonChinese) 15 else 20 // 英文/日文每行更少字符
} else {
20
}
val textLines = if (textContent.isNotEmpty()) {
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行
} else {
0
}
val baseHeight = 200.dp
val singleLineTextHeight = 24.dp // 增加高度以适应英文/日文
val doubleLineTextHeight = 44.dp // 增加高度以适应英文/日文
val authorInfoHeight = 25.dp
val paddingHeight = 10.dp
val paddingHeight2 = 3.dp
val totalHeight = baseHeight + when (textLines) {
0 -> authorInfoHeight + paddingHeight
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
else -> doubleLineTextHeight + authorInfoHeight + paddingHeight2
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f)
.padding(2.dp) .padding(2.dp)
.noRippleClickable { .noRippleClickable {
debouncer { debouncer {
@@ -147,14 +183,78 @@ fun DiscoverView() {
} }
} }
) { ) {
CustomAsyncImage( Column(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxWidth()
context = context, .background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
showShimmer = true ) {
) CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier
.fillMaxWidth()
.height(baseHeight)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
context = context,
showShimmer = true
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
// 文本内容区域,限制最大高度
if (momentItem.momentTextContent.isNotEmpty()) {
androidx.compose.material3.Text(
text = momentItem.momentTextContent,
modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp,
color = AppColors.text,
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
lineHeight = 16.sp // 设置行高以适应不同语言
)
}
// 使用 Spacer 确保头像昵称栏始终在底部,有足够的空间
Spacer(modifier = Modifier.weight(1f))
// 头像昵称栏,确保始终完整显示,设置最小高度避免被挤压
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 25.dp) // 最小高度确保完整显示,自适应避免被挤压
.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
imageUrl = momentItem.avatar,
contentDescription = "",
modifier = Modifier
.size(16.dp)
.clip(RoundedCornerShape(8.dp)),
context = context,
showShimmer = true
)
androidx.compose.material3.Text(
text = momentItem.nickname,
modifier = Modifier
.padding(start = 4.dp)
.weight(1f),
fontSize = 11.sp,
color = AppColors.text.copy(alpha = 0.6f),
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
}
if (momentItem.images.size > 1) { if (momentItem.images.size > 1) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -0,0 +1,205 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
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
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.formatPostTime2
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FullArticleModal(
moment: MomentEntity,
onDismiss: () -> Unit
) {
val appColors = LocalAppTheme.current
val context = LocalContext.current
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.9f // 90% 高度
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
// 滚动内容
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
// 新闻图片区域 - 固定高度和宽度
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.background(color = appColors.secondaryBackground)
) {
if (moment.images.isNotEmpty()) {
val firstImage = moment.images[0]
CustomAsyncImage(
context = context,
imageUrl = firstImage.url,
contentDescription = "新闻图片",
contentScale = ContentScale.Fit,
blurHash = firstImage.blurHash,
modifier = Modifier.fillMaxSize()
)
} else {
Image(
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
contentDescription = "默认图片",
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 新闻标题
Text(
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
lineHeight = 28.sp,
modifier = Modifier.padding(horizontal = 10.dp)
)
Spacer(modifier = Modifier.height(12.dp))
// 新闻来源和发布时间
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 来源按钮
Button(
onClick = { },
modifier = Modifier.height(28.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF7c68ef)
),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp),
shape = RoundedCornerShape(14.dp)
) {
Text(
text = if (moment.newsSource.isNotEmpty()) moment.newsSource else moment.nickname,
fontSize = 12.sp,
color = Color.White,
)
}
// 发布时间
Text(
text = moment.time.formatPostTime2(),
fontSize = 12.sp,
color = appColors.secondaryText
)
}
Spacer(modifier = Modifier.height(16.dp))
// 帖子内容
NewsContent(
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
images = moment.images,
context = context
)
Spacer(modifier = Modifier.height(200.dp))
}
}
}
}
@Composable
private fun NewsContent(
content: String,
images: List<com.aiosman.ravenow.entity.MomentImageEntity>,
context: android.content.Context
) {
val appColors = LocalAppTheme.current
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = content,
fontSize = 16.sp,
color = appColors.text,
lineHeight = 24.sp
)
// 图片内容
if (images.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
images.forEach { image ->
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
) {
CustomAsyncImage(
context = context,
imageUrl = image.url,
contentDescription = "内容图片",
contentScale = ContentScale.Fit,
blurHash = image.blurHash,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,306 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
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
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.graphics.ColorFilter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.CommentService
import com.aiosman.ravenow.data.CommentServiceImpl
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
import com.aiosman.ravenow.ui.composables.debouncedClickable
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.post.CommentContent
import com.aiosman.ravenow.ui.post.CommentMenuModal
import com.aiosman.ravenow.ui.post.CommentsViewModel
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
import kotlinx.coroutines.launch
class NewsCommentModalViewModel(
val postId: Int?
) : ViewModel() {
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString())
var commentService: CommentService = CommentServiceImpl()
init {
commentsViewModel.preTransit()
}
fun likeComment(commentId: Int) {
viewModelScope.launch {
commentsViewModel.likeComment(commentId)
}
}
fun unlikeComment(commentId: Int) {
viewModelScope.launch {
commentsViewModel.unlikeComment(commentId)
}
}
fun createComment(
content: String,
parentCommentId: Int? = null,
replyUserId: Int? = null,
replyCommentId: Int? = null
) {
viewModelScope.launch {
commentsViewModel.createComment(
content = content,
parentCommentId = parentCommentId,
replyUserId = replyUserId,
replyCommentId = replyCommentId
)
}
}
fun deleteComment(commentId: Int) {
commentsViewModel.deleteComment(commentId)
}
}
// 新闻评论弹窗
// @param postId 新闻帖子ID
// @param commentCount 评论数量
// @param onDismiss 关闭回调
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewsCommentModal(
postId: Int? = null,
commentCount: Int = 0,
onDismiss: () -> Unit = {},
onCommentAdded: () -> Unit = {},
onCommentDeleted: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val debouncedNavigation = rememberDebouncedNavigation()
// 实时评论数状态
var currentCommentCount by remember { mutableStateOf(commentCount) }
val model = viewModel<NewsCommentModalViewModel>(
key = "NewsCommentModalViewModel_$postId",
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NewsCommentModalViewModel(postId) as T
}
}
)
val commentViewModel = model.commentsViewModel
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
var showCommentMenu by remember { mutableStateOf(false) }
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
// 菜单弹窗
if (showCommentMenu) {
ModalBottomSheet(
onDismissRequest = {
showCommentMenu = false
},
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {
showCommentMenu = false
contextComment?.let {
model.deleteComment(it.id)
onCommentDeleted()
currentCommentCount = (currentCommentCount - 1).coerceAtLeast(0)
}
},
commentEntity = contextComment,
onCloseClick = {
showCommentMenu = false
},
isSelf = AppState.UserId?.toLong() == contextComment?.author,
onLikeClick = {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
showCommentMenu = false
contextComment?.let {
if (it.liked) {
model.unlikeComment(it.id)
} else {
model.likeComment(it.id)
}
}
}
},
onReplyClick = {
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
showCommentMenu = false
replyComment = contextComment
}
}
)
}
}
Column(
modifier = Modifier.background(AppColors.background)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${currentCommentCount}条评论",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
// 排序选择
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
OrderSelectionComponent {
commentViewModel.order = it
commentViewModel.reloadComment()
}
}
}
// 评论列表
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Box(
modifier = Modifier.fillMaxWidth()
) {
LazyColumn {
item {
CommentContent(
viewModel = commentViewModel,
onLongClick = { comment ->
showCommentMenu = true
contextComment = comment
},
onReply = { parentComment, _, _, _ ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
replyComment = parentComment
}
}
)
}
}
}
}
// 底部输入栏
Column(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
) {
HorizontalDivider(color = AppColors.inputBackground)
EditCommentBottomModal(
replyComment = replyComment,
autoFocus = false
) {
if (replyComment != null) {
if (replyComment?.parentCommentId != null) {
// 第三级评论
model.createComment(
content = it,
parentCommentId = replyComment?.parentCommentId,
replyUserId = replyComment?.author?.toInt(),
replyCommentId = replyComment?.id
)
} else {
// 子级评论
model.createComment(
content = it,
parentCommentId = replyComment?.id,
replyCommentId = replyComment?.id
)
}
} else {
// 顶级评论
model.createComment(content = it)
}
replyComment = null
onCommentAdded()
currentCommentCount++
}
Spacer(modifier = Modifier.height(navBarHeight))
}
}
}

View File

@@ -0,0 +1,436 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
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
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.statusBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
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.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalConfiguration
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.exp.formatPostTime2
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.index.tabs.moment.tabs.news.NewsViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun NewsScreen() {
val model = NewsViewModel
val moments = model.moments
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
// 评论弹窗状态
var showCommentModal by remember { mutableStateOf(false) }
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
// 查看全文弹窗状态
var showFullArticleModal by remember { mutableStateOf(false) }
var selectedArticleMoment by remember { mutableStateOf<MomentEntity?>(null) }
// 垂直翻页状态
val pagerState = rememberPagerState(pageCount = { moments.size })
// 防抖器
val likeDebouncer = rememberDebouncer()
val favoriteDebouncer = rememberDebouncer()
// 初始化加载数据
LaunchedEffect(Unit) {
model.refreshPager()
}
// 监听数据变化,重置加载状态
LaunchedEffect(moments.size) {
// 当数据增加时如果接近列表末尾Pager会自动更新页数
}
// 当翻页接近末尾时加载更多
LaunchedEffect(pagerState.currentPage, moments.size) {
if (moments.isNotEmpty() && pagerState.currentPage >= moments.size - 2) {
model.loadMore()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(AppColors.background)
) {
if (moments.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "暂无新闻内容", color = AppColors.text, fontSize = 16.sp)
}
} else {
VerticalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
val momentItem = moments.getOrNull(page) ?: return@VerticalPager
NewsItem(
moment = momentItem,
modifier = Modifier.fillMaxSize(),
onCommentClick = {
selectedMoment = momentItem
showCommentModal = true
},
onReadFullClick = {
selectedArticleMoment = momentItem
showFullArticleModal = true
},
onLikeClick = {
likeDebouncer {
// 检查游客模式
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
}
}
}
}
},
onFavoriteClick = {
favoriteDebouncer {
// 检查游客模式
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
}
}
}
)
}
}
// 查看全文弹窗
if (showFullArticleModal && selectedArticleMoment != null) {
FullArticleModal(
moment = selectedArticleMoment!!,
onDismiss = {
showFullArticleModal = false
}
)
}
// 评论弹窗
if (showCommentModal && selectedMoment != null) {
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val sheetHeight = screenHeight * 0.67f // 三分之二高度
ModalBottomSheet(
onDismissRequest = {
showCommentModal = false
},
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
modifier = Modifier
.fillMaxWidth()
.height(sheetHeight),
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
NewsCommentModal(
postId = selectedMoment?.id,
commentCount = selectedMoment?.commentCount ?: 0,
onDismiss = {
showCommentModal = false
},
onCommentAdded = {
selectedMoment?.id?.let { model.onAddComment(it) }
},
onCommentDeleted = {
selectedMoment?.id?.let { model.onDeleteComment(it) }
}
)
}
}
}
}
//单个新闻项
@Composable
fun NewsItem(
moment: MomentEntity,
modifier: Modifier = Modifier,
onCommentClick: () -> Unit = {},
onReadFullClick: () -> Unit = {},
onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
Column(
modifier = modifier
.fillMaxSize()
.background(AppColors.background)
.padding(vertical = 8.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = 30.dp)
) {
// 新闻图片
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(horizontal = 16.dp)
) {
if (moment.images.isNotEmpty()) {
CustomAsyncImage(
context = context,
imageUrl = moment.images[0].thumbnail,
contentDescription = "新闻图片",
contentScale = ContentScale.Crop,
blurHash = moment.images[0].blurHash,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(8.dp))
)
} else {
Image(
painter = androidx.compose.ui.res.painterResource(id = R.drawable.default_moment_img),
contentDescription = "默认图片",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(8.dp))
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// 新闻标题
Text(
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 新闻内容(超出使用省略号)
Text(
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp,
maxLines = 6,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
// 新闻信息
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 来源和时间(显示月份与具体时间)
Text(
text = if (moment.newsSource.isNotEmpty()) "${moment.newsSource}${moment.time.formatPostTime2()}" else "${moment.nickname}${moment.time.formatPostTime2()}",
fontSize = 12.sp,
color = AppColors.secondaryText,
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// 查看全文
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.noRippleClickable { onReadFullClick() }
) {
Text(
text = stringResource(R.string.read_full_article),
fontSize = 13.sp,
fontWeight = FontWeight.W600,
color = Color(0xFF7c45ed)
)
Spacer(modifier = Modifier.width(4.dp))
Image(
painter = androidx.compose.ui.res.painterResource(id = R.mipmap.arrow),
contentDescription = "箭头",
modifier = Modifier.size(18.dp),
colorFilter = ColorFilter.tint(Color(0xFF7c45ed))
)
}
}
}
// 互动栏
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 25.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 点赞
NewsActionButton(
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
count = moment.likeCount.toString(),
isActive = moment.liked,
modifier = Modifier.noRippleClickable { onLikeClick() }
)
// 评论
NewsActionButton(
icon = R.mipmap.icon_comment,
count = moment.commentCount.toString(),
isActive = false,
modifier = Modifier.noRippleClickable { onCommentClick() }
)
// 收藏
NewsActionButton(
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
count = moment.favoriteCount.toString(),
isActive = moment.isFavorite,
modifier = Modifier.noRippleClickable { onFavoriteClick() }
)
// 分享
NewsActionButton(
icon = R.mipmap.icon_share,
count = "",
isActive = false,
text = stringResource(R.string.share),
textSize = 8.sp
)
}
}
}
// 互动栏按钮
@Composable
fun NewsActionButton(
icon: Int,
count: String,
isActive: Boolean,
modifier: Modifier = Modifier,
text: String? = null,
textSize: androidx.compose.ui.unit.TextUnit = 12.sp
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.width(60.dp)
.background(
color = AppColors.secondaryBackground,
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
painter = androidx.compose.ui.res.painterResource(id = icon),
contentDescription = "操作图标",
modifier = Modifier.size(16.dp)
)
if (count.isNotEmpty()) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = count,
fontSize = 12.sp,
color = AppColors.text
)
}
if (text != null) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = text,
fontSize = textSize,
color = AppColors.text
)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
object NewsViewModel : BaseMomentModel() {
override fun extraArgs(): MomentLoaderExtraArgs {
// 只拉取新闻
return MomentLoaderExtraArgs(
explore = false,
timelineId = null,
authorId = null,
newsOnly = true
)
}
}

View File

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

View File

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

View File

@@ -43,6 +43,8 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.MomentCard import com.aiosman.ravenow.ui.composables.MomentCard
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext
/** /**
* 动态列表 * 动态列表
@@ -76,7 +78,49 @@ fun TimelineMomentsList() {
model.loadMore() model.loadMore()
} }
} }
if (moments.isEmpty()) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 188.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
val exploreDebouncer = rememberDebouncer()
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ExploreButton(
onClick = {
exploreDebouncer {
/* TODO: 添加点击事件处理 */
} }
)
}
}
} else if (moments.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -91,7 +135,7 @@ fun TimelineMomentsList() {
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img id = if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img
else R.mipmap.qst_gz_qs_img), else R.mipmap.invalid_name_4),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -191,9 +235,9 @@ fun ExploreButton(
) { ) {
val gradientBrush = Brush.linearGradient( val gradientBrush = Brush.linearGradient(
colors = listOf( colors = listOf(
Color(0xFFee2a33), Color(0xFF7c45ed),
Color(0xFFd80264), Color(0xFF7c68ef),
Color(0xFF664c92) Color(0xFF7bd8f8)
) )
) )

View File

@@ -0,0 +1,178 @@
package com.aiosman.ravenow.ui.index.tabs.profile
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.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.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 com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
/**
* 拉黑确认弹窗
*/
@Composable
fun BlockConfirmDialog(
userProfile: AccountProfileEntity?,
onConfirmBlock: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.padding(24.dp)
) {
// 用户头像
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp)
) {
CustomAsyncImage(
LocalContext.current,
userProfile?.avatar,
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.background(
color = AppColors.background,
shape = CircleShape
),
contentDescription = "用户头像",
contentScale = ContentScale.Crop
)
}
// 确认文本
Text(
text = stringResource(R.string.confirm_block_user, userProfile?.nickName ?: "该用户"),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 24.dp)
)
// 说明信息
Column(
modifier = Modifier.padding(bottom = 32.dp)
) {
// 第一条说明
Row(
modifier = Modifier.padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_off_eye),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_1),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
// 第二条说明
Row(
modifier = Modifier.padding(bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_notice_mute),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_2),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
// 第三条说明
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_off_bell),
contentDescription = "",
tint = AppColors.text,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.block_description_3),
fontSize = 14.sp,
color = AppColors.text,
lineHeight = 20.sp
)
}
}
// 确认拉黑按钮
androidx.compose.material3.Button(
onClick = {
onConfirmBlock()
onDismiss()
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
stringResource(R.string.block),
color = AppColors.background,
fontSize = 16.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@@ -37,9 +37,18 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils
@Composable @Composable
fun GalleryItem( fun GalleryItem(
moment: MomentEntity, moment: MomentEntity,
@@ -129,8 +138,50 @@ fun GalleryGrid(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
val debouncer = rememberDebouncer() val debouncer = rememberDebouncer()
var refreshKey by remember { mutableStateOf(0) }
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (moments.isEmpty()) { if (!isNetworkAvailable) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
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 = {
refreshKey++
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
} else if (moments.isEmpty()) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -140,28 +191,37 @@ fun GalleryGrid(
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img id = if(AppState.darkMode) R.mipmap.shuihu_dark
else R.mipmap.qs_dt_qs_img), else R.mipmap.invalid_name_7),
contentDescription = "暂无图片", contentDescription = "暂无图片",
modifier = Modifier.size(181.dp), modifier = Modifier
.size(width = 181.dp, height = 153.dp),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "故事还没开始", text = stringResource(R.string.your_story_not_started),
fontSize = 16.sp, fontSize = 16.sp,
color = AppColors.text, color = AppColors.text,
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "发布一条动态,和世界打个招呼吧", text = stringResource(R.string.publish_moment_greeting),
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
) )
} }
} else { } else {
@@ -171,7 +231,7 @@ fun GalleryGrid(
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp), modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
) { ) {
itemsIndexed(moments) { idx, moment -> itemsIndexed(moments) { idx, moment ->
if (moment != null) { if (moment != null && moment.images.isNotEmpty()) {
val itemDebouncer = rememberDebouncer() val itemDebouncer = rememberDebouncer()
Box( Box(
modifier = Modifier modifier = Modifier

View File

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

View File

@@ -50,8 +50,8 @@ fun OtherProfileAction(
// 定义渐变色 // 定义渐变色
val followGradient = Brush.horizontalGradient( val followGradient = Brush.horizontalGradient(
colors = listOf( colors = listOf(
Color(0xFFE53E3E), // 红色 Color(0xFF7c45ed),
Color(0xFF9F7AEA) // 紫色 Color(0x777c68ef)
) )
) )
@@ -98,9 +98,9 @@ fun OtherProfileAction(
} }
) { ) {
Text( Text(
text = if (profile.isFollowing) "已关注" else stringResource(R.string.follow_upper), text = if (profile.isFollowing) stringResource(R.string.follow_upper_had) else stringResource(R.string.follow_upper),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = if (profile.isFollowing) { color = if (profile.isFollowing) {
// 已关注状态 - 灰色文字 // 已关注状态 - 灰色文字
AppColors.text.copy(alpha = 0.6f) AppColors.text.copy(alpha = 0.6f)
@@ -133,11 +133,37 @@ fun OtherProfileAction(
Text( Text(
text = stringResource(R.string.chat_upper), text = stringResource(R.string.chat_upper),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W900,
color = AppColors.text, // 使用主题文字颜色 color = AppColors.text, // 使用主题文字颜色
) )
} }
} }
// 分享按钮 - 灰色背景样式
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.nonActive)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
// TODO: 添加分享逻辑
}
}
) {
Text(
text = stringResource(R.string.share),
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text, // 使用主题文字颜色
)
}
} }
} }

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -29,11 +30,13 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
fun SelfProfileAction( fun SelfProfileAction(
onEditProfile: () -> Unit, onEditProfile: () -> Unit,
onPremiumClick: (() -> Unit)? = null onPremiumClick: (() -> Unit),
onShare: (() -> Unit)? = null
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val editProfileDebouncer = rememberDebouncer() val editProfileDebouncer = rememberDebouncer()
val premiumClickDebouncer = rememberDebouncer() val premiumClickDebouncer = rememberDebouncer()
val shareDebouncer = rememberDebouncer()
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -45,52 +48,101 @@ fun SelfProfileAction(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.weight(1f) .width(60.dp).height(25.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(12.dp))
.background(AppColors.nonActive) .background(androidx.compose.ui.graphics.Color(0x229284BD))
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
editProfileDebouncer { editProfileDebouncer {
onEditProfile() onEditProfile()
} }
} }
) { ) {
Image(
painter = painterResource(id = R.mipmap.fill_and_sign),
contentDescription = "",
modifier = Modifier.size(12.dp),
colorFilter = ColorFilter.tint(androidx.compose.ui.graphics.Color(0xFF9284BD))
)
Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = stringResource(R.string.edit_profile), text = stringResource(R.string.edit_profile),
fontSize = 14.sp, fontSize = 12.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,
color = AppColors.text, color = androidx.compose.ui.graphics.Color(0xFF9284BD),
) )
} }
// Rave Premium 按钮(右侧) // // 预留按钮位置
Row( // Row(
verticalAlignment = Alignment.CenterVertically, // verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, // horizontalArrangement = Arrangement.Center,
modifier = Modifier // modifier = Modifier
.weight(1f) // .weight(1f)
.clip(RoundedCornerShape(8.dp)) // .clip(RoundedCornerShape(10.dp))
.background(AppColors.premiumBackground) // .padding(horizontal = 16.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp) // .noRippleClickable {
.noRippleClickable { //
premiumClickDebouncer { // }
onPremiumClick?.invoke() // ) {
} // Text(
} // text = "",
) { // fontSize = 14.sp,
Image( // fontWeight = FontWeight.W900,
painter = painterResource(id = R.drawable.ic_member), // color = AppColors.text,
contentDescription = "", // )
modifier = Modifier.size(18.dp), // }
colorFilter = ColorFilter.tint(AppColors.premiumText) //
) // // 分享按钮
Spacer(modifier = Modifier.width(8.dp)) // Row(
Text( // verticalAlignment = Alignment.CenterVertically,
text = "Rave Premium", // horizontalArrangement = Arrangement.Center,
fontSize = 14.sp, // modifier = Modifier
fontWeight = FontWeight.W600, // .weight(1f)
color = AppColors.premiumText, // .clip(RoundedCornerShape(10.dp))
) // .background(AppColors.nonActive)
} // .padding(horizontal = 16.dp, vertical = 12.dp)
// .noRippleClickable {
// shareDebouncer {
// // TODO: 添加分享逻辑
// }
// }
// ) {
// Text(
// text = stringResource(R.string.share),
// fontSize = 14.sp,
// fontWeight = FontWeight.W900,
// color = AppColors.text,
// )
// }
// // Rave Premium 按钮(右侧)
// Row(
// verticalAlignment = Alignment.CenterVertically,
// horizontalArrangement = Arrangement.Center,
// modifier = Modifier
// .weight(1f)
// .clip(RoundedCornerShape(8.dp))
// .background(AppColors.premiumBackground)
// .padding(horizontal = 16.dp, vertical = 12.dp)
// .noRippleClickable {
// premiumClickDebouncer {
// onPremiumClick?.invoke()
// }
// }
// ) {
// Image(
// painter = painterResource(id = R.drawable.ic_member),
// contentDescription = "",
// modifier = Modifier.size(18.dp),
// colorFilter = ColorFilter.tint(AppColors.premiumText)
// )
// Spacer(modifier = Modifier.width(8.dp))
// Text(
// text = "Rave Premium",
// fontSize = 14.sp,
// fontWeight = FontWeight.W600,
// color = AppColors.premiumText,
// )
// }
} }
} }

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -29,9 +30,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -44,7 +47,10 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils
@Composable @Composable
fun UserAgentsList( fun UserAgentsList(
@@ -55,15 +61,14 @@ fun UserAgentsList(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
LazyColumn( if (agents.isEmpty()) {
modifier = modifier.fillMaxSize(), // 使用带分段控制器的空状态布局
verticalArrangement = Arrangement.spacedBy(8.dp) AgentEmptyContentWithSegments()
) { } else {
if (agents.isEmpty()) { LazyColumn(
item { modifier = modifier.fillMaxSize(),
EmptyAgentsView() verticalArrangement = Arrangement.spacedBy(8.dp)
} ) {
} else {
items(agents) { agent -> items(agents) { agent ->
UserAgentCard( UserAgentCard(
agent = agent, agent = agent,
@@ -71,11 +76,11 @@ fun UserAgentsList(
onAvatarClick = onAvatarClick onAvatarClick = onAvatarClick
) )
} }
}
// 底部间距
// 底部间距 item {
item { Spacer(modifier = Modifier.height(120.dp))
Spacer(modifier = Modifier.height(120.dp)) }
} }
} }
} }
@@ -88,7 +93,7 @@ fun UserAgentCard(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val navController = LocalNavController.current val navController = LocalNavController.current
// 防抖状态 // 防抖状态
var lastClickTime by remember { mutableStateOf(0L) } var lastClickTime by remember { mutableStateOf(0L) }
@@ -193,9 +198,192 @@ fun UserAgentCard(
} }
} }
@Composable
fun AgentEmptyContentWithSegments() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
// 分段控制器
AgentSegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 空状态内容(使用智能体原本的图标和文字)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isNetworkAvailable) {
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai),
contentDescription = "暂无Agent",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
// 根据是否为深色模式调整间距
Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
Text(
text = stringResource(R.string.exclusive_ai_waiting),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.ai_companion_not_tool),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
}
}
}
@Composable
private fun AgentSegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Row(
modifier = modifier
.height(32.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
// 全部
AgentSegmentButton(
text = stringResource(R.string.chat_all),
isSelected = selectedIndex == 0,
onClick = { onSegmentSelected(0) },
width = 54.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 公开
AgentSegmentButton(
text = stringResource(R.string.public_label),
isSelected = selectedIndex == 1,
onClick = { onSegmentSelected(1) },
width = 59.dp,
appColors = AppColors
)
Spacer(modifier = Modifier.width(8.dp))
// 私有
AgentSegmentButton(
text = stringResource(R.string.private_label),
isSelected = selectedIndex == 2,
onClick = { onSegmentSelected(2) },
width = 54.dp,
appColors = AppColors
)
}
}
@Composable
private fun AgentSegmentButton(
text: String,
isSelected: Boolean,
onClick: () -> Unit,
width: androidx.compose.ui.unit.Dp,
appColors: com.aiosman.ravenow.AppThemeData
) {
Box(
modifier = Modifier
.width(width)
.height(32.dp)
.background(
color = if (isSelected) {
appColors.checkedBackground // 使用选中背景色(暗色模式下是白色,亮色模式下是黑色)
} else {
Color(0x147C7480) // RGB(124, 116, 128, alpha 0.08)
},
shape = RoundedCornerShape(1000.dp)
)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) {
appColors.checkedText // 选中时使用选中文本颜色(暗色模式下是黑色,亮色模式下是白色)
} else {
appColors.text // 未选中时使用文本颜色
}
)
}
}
@Composable @Composable
fun EmptyAgentsView() { fun EmptyAgentsView() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
Column( Column(
modifier = Modifier modifier = Modifier
@@ -203,30 +391,66 @@ fun EmptyAgentsView() {
.padding(vertical = 60.dp), .padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( if (isNetworkAvailable) {
painter = painterResource( Image(
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img painter = painterResource(
else R.mipmap.qs_ai_qs_img), id = if(AppState.darkMode) R.mipmap.ai_dark
contentDescription = "暂无Agent", else R.mipmap.ai),
modifier = Modifier.size(181.dp), contentDescription = "暂无Agent",
) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
Spacer(modifier = Modifier.height(24.dp)) .align(Alignment.CenterHorizontally),
)
Text(
text = "专属AI等你召唤", // 根据是否为深色模式调整间距
fontSize = 16.sp, Spacer(modifier = Modifier.height(if(AppState.darkMode) 9.dp else 24.dp))
color = AppColors.text,
fontWeight = FontWeight.W600 Text(
) text = "专属AI等你召唤",
fontSize = 16.sp,
Spacer(modifier = Modifier.height(8.dp)) color = AppColors.text,
fontWeight = FontWeight.W600
Text( )
text = "AI将成为你的伙伴而不是工具",
fontSize = 14.sp, Spacer(modifier = Modifier.height(8.dp))
color = AppColors.secondaryText,
fontWeight = FontWeight.W400 Text(
) text = "AI将成为你的伙伴而不是工具",
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)
}
)
}
} }
} }

View File

@@ -65,94 +65,94 @@ fun UserAgentsRow(
viewModel.loadUserAgents(userId) viewModel.loadUserAgents(userId)
} }
// 总是显示智能体区域,即使没有数据也显示标题和状态 // // 总是显示智能体区域,即使没有数据也显示标题和状态
Column( // Column(
modifier = modifier // modifier = modifier
.fillMaxWidth() // .fillMaxWidth()
.padding(horizontal = 16.dp) // .padding(horizontal = 16.dp)
) { // ) {
Text( // Text(
text = if (isSelf) "我的智能体" else "TA的智能体", // text = if (isSelf) "我的智能体" else "TA的智能体",
fontSize = 16.sp, // fontSize = 16.sp,
fontWeight = FontWeight.W600, // fontWeight = FontWeight.W600,
color = AppColors.text, // color = AppColors.text,
modifier = Modifier.padding(bottom = 12.dp) // modifier = Modifier.padding(bottom = 12.dp)
) // )
//
when { // when {
viewModel.isLoading -> { // viewModel.isLoading -> {
// 显示加载状态 // // 显示加载状态
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(60.dp), // .height(60.dp),
contentAlignment = Alignment.Center // contentAlignment = Alignment.Center
) { // ) {
Text( // Text(
text = "加载中...", // text = "加载中...",
fontSize = 14.sp, // fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f) // color = AppColors.text.copy(alpha = 0.6f)
) // )
} // }
} // }
viewModel.error != null -> { // viewModel.error != null -> {
// 显示错误状态 // // 显示错误状态
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(60.dp), // .height(60.dp),
contentAlignment = Alignment.Center // contentAlignment = Alignment.Center
) { // ) {
Text( // Text(
text = "加载失败: ${viewModel.error}", // text = "加载失败: ${viewModel.error}",
fontSize = 14.sp, // fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f) // color = AppColors.text.copy(alpha = 0.6f)
) // )
} // }
} // }
viewModel.agents.isEmpty() -> { // viewModel.agents.isEmpty() -> {
// 显示空状态 // // 显示空状态
Box( // Box(
modifier = Modifier // modifier = Modifier
.fillMaxWidth() // .fillMaxWidth()
.height(60.dp), // .height(60.dp),
contentAlignment = Alignment.Center // contentAlignment = Alignment.Center
) { // ) {
Text( // Text(
text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体", // text = if (isSelf) "您还没有创建智能体" else "TA还没有创建智能体",
fontSize = 14.sp, // fontSize = 14.sp,
color = AppColors.text.copy(alpha = 0.6f) // color = AppColors.text.copy(alpha = 0.6f)
) // )
} // }
} // }
else -> { // else -> {
// 显示智能体列表 // // 显示智能体列表
LazyRow( // LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp), // horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth() // modifier = Modifier.fillMaxWidth()
) { // ) {
// 显示智能体项目 // // 显示智能体项目
items(viewModel.agents) { agent -> // items(viewModel.agents) { agent ->
AgentItem( // AgentItem(
agent = agent, // agent = agent,
onClick = { onAgentClick(agent) }, // onClick = { onAgentClick(agent) },
onAvatarClick = { onAvatarClick(agent) }, // onAvatarClick = { onAvatarClick(agent) },
onLongClick = { onAgentLongClick(agent) } // onLongClick = { onAgentLongClick(agent) }
) // )
} // }
//
// 添加"更多"按钮 // // 添加"更多"按钮
item { // item {
MoreAgentItem( // MoreAgentItem(
onClick = onMoreClick // onClick = onMoreClick
) // )
} // }
} // }
} // }
} // }
//
Spacer(modifier = Modifier.height(16.dp)) // Spacer(modifier = Modifier.height(16.dp))
} // }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View File

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

View File

@@ -1,26 +1,39 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
@@ -29,23 +42,52 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import java.text.NumberFormat
import java.util.Locale
@Composable @Composable
fun UserItem( fun UserItem(
accountProfileEntity: AccountProfileEntity, accountProfileEntity: AccountProfileEntity,
postCount: Int = 0 postCount: Long = 0,
isSelf: Boolean = false,
onEditClick: () -> Unit = {}
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val followerDebouncer = rememberDebouncer() val followerDebouncer = rememberDebouncer()
val followingDebouncer = rememberDebouncer() val followingDebouncer = rememberDebouncer()
// 获取 MBTI 和星座信息
val mbti = remember(accountProfileEntity.id) {
AppStore.getUserMbti(accountProfileEntity.id)
}
val zodiac = remember(accountProfileEntity.id) {
AppStore.getUserZodiac(accountProfileEntity.id)
}
// 格式化粉丝数
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
val formattedFollowerCount = remember(accountProfileEntity.followerCount) {
if (accountProfileEntity.followerCount >= 10000) {
val wan = accountProfileEntity.followerCount / 10000.0
if (wan >= 100) {
"${wan.toInt()}"
} else {
String.format("%.1f万", wan)
}
} else {
numberFormat.format(accountProfileEntity.followerCount)
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
// 顶部:头像和统计数据
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// 头像 // 头像
CustomAsyncImage( CustomAsyncImage(
@@ -53,39 +95,47 @@ fun UserItem(
accountProfileEntity.avatar, accountProfileEntity.avatar,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(48.dp), .size(96.dp),
contentDescription = "", contentDescription = "",
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Spacer(modifier = Modifier.width(32.dp))
//个人统计 // 统计数据
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f) horizontalArrangement = Arrangement.spacedBy(0.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
// 帖子数 // 帖子数
Column( Column(
modifier = Modifier
.width(80.dp)
.height(40.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(1f) verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = postCount.toString(), text = postCount.toString(),
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "帖子", text = stringResource(R.string.posts),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
// 粉丝数 // 粉丝数
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.weight(1f) .width(80.dp)
.height(40.dp)
.noRippleClickable { .noRippleClickable {
followerDebouncer { followerDebouncer {
navController.navigate( navController.navigate(
@@ -95,26 +145,33 @@ fun UserItem(
) )
) )
} }
} },
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = accountProfileEntity.followerCount.toString(), text = formattedFollowerCount,
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "粉丝", text = stringResource(R.string.followers_upper),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
// 关注数 // 关注数
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.weight(1f) .width(80.dp)
.height(40.dp)
.offset(x = 6.dp)
.noRippleClickable { .noRippleClickable {
followingDebouncer { followingDebouncer {
navController.navigate( navController.navigate(
@@ -124,49 +181,161 @@ fun UserItem(
) )
) )
} }
} },
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = accountProfileEntity.followingCount.toString(), text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.W600, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = 15.sp,
color = AppColors.text color = AppColors.text,
textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "关注", text = stringResource(R.string.following_upper),
color = AppColors.text fontWeight = FontWeight.Normal,
fontSize = 11.sp,
color = AppColors.text,
textAlign = TextAlign.Center
) )
} }
} }
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// 昵称
Text( // 中间:昵称、简介、创建者信息
text = accountProfileEntity.nickName, Column(
fontWeight = FontWeight.W600, modifier = Modifier
fontSize = 16.sp, .fillMaxWidth()
color = AppColors.text ) {
) // 昵称
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()){
Text( Text(
text = accountProfileEntity.bio, text = accountProfileEntity.nickName,
fontSize = 14.sp, fontWeight = FontWeight.Bold,
color = AppColors.secondaryText, fontSize = 22.sp,
maxLines = 1, letterSpacing = (-0.3).sp,
overflow = TextOverflow.Ellipsis color = AppColors.text
) )
}else{
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
if (accountProfileEntity.bio.isNotEmpty()) {
Text(
text = accountProfileEntity.bio,
fontSize = 13.sp,
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
Text(
text = "Welcome to my fantiac word i will show you something about magic",
fontSize = 13.sp,
color = AppColors.secondaryText,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// 创建者信息(如果是 AI 账户,可以显示创建者)
// 注意:当前 AccountProfileEntity 没有创建者字段,这里暂时留空
// 如果需要显示,需要从其他地方获取创建者信息
}
Spacer(modifier = Modifier.height(12.dp))
// 底部:标签按钮
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// MBTI 标签
if (!mbti.isNullOrEmpty()) {
ProfileTag(
text = mbti,
backgroundColor = Color(0x33FF8D28), // 255/255, 141/255, 40/255, alpha 0.2
textColor = AppColors.text
)
}
// 星座标签
if (!zodiac.isNullOrEmpty()) {
ProfileTag(
text = zodiac,
backgroundColor = Color(0x33FFCC00), // 255/255, 204/255, 0/255, alpha 0.2
textColor = AppColors.text
)
}
// 编辑标签(仅自己可见)
if (isSelf) {
ProfileTag(
text = stringResource(R.string.edit_profile),
backgroundColor = Color(0x14947A80), // 124/255, 116/255, 128/255, alpha 0.08
textColor = AppColors.text,
leadingIcon = {
EditIcon(
color = AppColors.text,
modifier = Modifier.size(16.dp)
)
},
onClick = onEditClick
)
}
}
}
}
@Composable
private fun ProfileTag(
text: String,
backgroundColor: Color,
textColor: Color,
leadingIcon: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null
) {
Box(
modifier = Modifier
.height(25.dp)
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
)
.padding(horizontal = 8.dp, vertical = 4.dp),
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
leadingIcon?.invoke()
Text( Text(
text = "No bio here.", text = text,
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = textColor
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
} }
}
@Composable
private fun EditIcon(
color: Color,
modifier: Modifier = Modifier
) {
Image(
painter = painterResource(id = R.mipmap.bi),
contentDescription = stringResource(R.string.edit_profile),
modifier = modifier,
colorFilter = ColorFilter.tint(color)
)
} }

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -28,6 +29,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Tab import androidx.compose.material.Tab
import androidx.compose.material.TabRow import androidx.compose.material.TabRow
@@ -44,6 +47,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -53,6 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -70,6 +75,7 @@ import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -304,6 +310,7 @@ fun MomentResultTab() {
var dataFlow = model.momentsFlow var dataFlow = model.momentsFlow
var moments = dataFlow.collectAsLazyPagingItems() var moments = dataFlow.collectAsLazyPagingItems()
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -317,10 +324,13 @@ fun MomentResultTab() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.syss_yh_qs_img), else R.mipmap.invalid_name_1),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -337,6 +347,33 @@ fun MomentResultTab() {
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400
) )
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -369,6 +406,7 @@ fun UserResultTab() {
val model = SearchViewModel val model = SearchViewModel
val users = model.usersFlow.collectAsLazyPagingItems() val users = model.usersFlow.collectAsLazyPagingItems()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -380,10 +418,13 @@ fun UserResultTab() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image( androidx.compose.foundation.Image(
painter = painterResource( painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.syss_yh_qs_img), else R.mipmap.invalid_name_1),
contentDescription = "No Comment", contentDescription = "No Comment",
modifier = Modifier.size(140.dp) modifier = Modifier.size(140.dp)
) )
@@ -400,6 +441,33 @@ fun UserResultTab() {
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.W400 fontWeight = FontWeight.W400
) )
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
}
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -497,3 +565,42 @@ fun UserItem(
} }
} }
} }
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
)
}
}
}

View File

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

View File

@@ -39,15 +39,14 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.AccountLikeEntity import com.aiosman.ravenow.entity.AccountLikeEntity
import com.aiosman.ravenow.exp.timeAgo import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder import com.aiosman.ravenow.ui.composables.BottomNavigationPlaceholder
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@Preview @Preview
@Composable @Composable
fun LikeNoticeScreen() { fun LikeNoticeScreen() {
@@ -72,18 +71,47 @@ fun LikeNoticeScreen() {
.background(color = AppColors.background) .background(color = AppColors.background)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
NoticeScreenHeader(
stringResource(R.string.like_upper),
moreIcon = false
)
}
if (likes.itemCount == 0) { val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
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),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
LikeNoticeViewModel.reload(force = true)
}
)
}
}
} else if (likes.itemCount == 0) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding(top=149.dp), .padding(top=149.dp),
@@ -95,14 +123,15 @@ fun LikeNoticeScreen() {
) { ) {
Image( Image(
painter = painterResource( painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_z_qs_as_img id = if(AppState.darkMode) R.mipmap.sanqiu_dark
else R.mipmap.qst_z_qs_img), else R.mipmap.invalid_name_6),
contentDescription = "No Notice", contentDescription = "No Notice",
modifier = Modifier.size(181.dp) modifier = Modifier
.size(width = 181.dp, height = 153.dp)
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
Text( Text(
text = "你的赞在路上", text = "你的赞在赶来的路上",
color = AppColors.text, color = AppColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600, fontWeight = FontWeight.W600,

View File

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

View File

@@ -6,9 +6,11 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -40,12 +42,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -70,6 +74,11 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
@Preview @Preview
@Composable @Composable
fun LoginPage() { fun LoginPage() {
@@ -209,14 +218,14 @@ fun LoginPage() {
saveData() saveData()
} }
// 显示成功提示 // // 显示成功提示
coroutineScope.launch(Dispatchers.Main) { // coroutineScope.launch(Dispatchers.Main) {
Toast.makeText( // Toast.makeText(
context, // context,
"游客登录成功", // "游客登录成功",
Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
).show() // ).show()
} // }
// 初始化应用状态游客模式会自动跳过推送和TRTC初始化 // 初始化应用状态游客模式会自动跳过推送和TRTC初始化
try { try {
@@ -260,13 +269,37 @@ fun LoginPage() {
.fillMaxSize() .fillMaxSize()
.background(AppColors.background) .background(AppColors.background)
) { ) {
Box( Row(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 60.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
val localContext = LocalContext.current // 获取 Context Box(
MovingImageWall(localContext.resources) // 将 resources 传递给 MovingImageWall modifier = Modifier
.size(30.dp)
.background(
color = AppColors.text.copy(alpha = 0.1f),
shape = androidx.compose.foundation.shape.CircleShape
)
.noRippleClickable {
guestLogin()
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "Close",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.text)
)
}
} }
Spacer(modifier = Modifier.height(20.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -274,77 +307,109 @@ fun LoginPage() {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Image( Box(
painter = painterResource(id = R.mipmap.invalid_name),
contentDescription = "Rave Now",
modifier = Modifier modifier = Modifier
.size(52.dp) .fillMaxWidth()
) .height(400.dp)
Spacer(modifier = Modifier.height(8.dp))
) {
val lottieFile = if (AppState.darkMode) "login.lottie" else "login_light.lottie"
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.fillMaxSize()
)
}
Text( Text(
"Rave Now", text = stringResource(R.string.join_party_carnival),
fontSize = 28.sp, fontSize = 17.sp,
fontWeight = FontWeight.W900, fontWeight = FontWeight.W600,
color = AppColors.text color = AppColors.text
) )
Spacer(modifier = Modifier.height(16.dp)) // Image(
Text( // painter = painterResource(id = R.mipmap.invalid_name),
"Your Night Starts Here", // contentDescription = "Rave Now",
fontSize = 20.sp, // modifier = Modifier
fontWeight = FontWeight.W700, // .size(52.dp)
color = AppColors.text // .clip(RoundedCornerShape(10.dp))
) // )
Spacer(modifier = Modifier.height(8.dp)) // Spacer(modifier = Modifier.height(8.dp))
// Text(
// "Rave Now",
// fontSize = 28.sp,
// fontWeight = FontWeight.W900,
// color = AppColors.text
// )
// Spacer(modifier = Modifier.height(16.dp))
// Text(
// "Your Night Starts Here",
// fontSize = 20.sp,
// fontWeight = FontWeight.W700,
// color = AppColors.text
// )
//注册tab
Spacer(modifier = Modifier.height(48.dp))
ActionButton( ActionButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_up_upper), text = stringResource(R.string.sign_up_upper),
color = AppColors.mainText, color = if (AppState.darkMode) Color.Black else Color.White,
backgroundColor = AppColors.main backgroundColor = if (AppState.darkMode) Color.White else Color.Black
) { ) {
navController.navigate( navController.navigate(
NavigationRoute.EmailSignUp.route, NavigationRoute.EmailSignUp.route,
) )
} }
if (showGoogleLogin) { //谷歌登录tab
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
ActionButton( ActionButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.border(
width = 1.5.dp,
color = if (AppState.darkMode) Color.White else Color.Black,
shape = RoundedCornerShape(24.dp)
),
text = stringResource(R.string.sign_in_with_google), text = stringResource(R.string.sign_in_with_google),
color = AppColors.text, color = if (AppState.darkMode) Color.White else Color.Black,
backgroundColor = if (AppState.darkMode) Color.Black else Color.White,
leading = { leading = {
Image( Image(
painter = painterResource(id = R.drawable.rider_pro_google), painter = painterResource(id = R.mipmap.rider_pro_signup_google),
contentDescription = "Google", contentDescription = "Google",
modifier = Modifier.size(36.dp) modifier = Modifier.size(18.dp),
) )
}, },
expandText = true, expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp) contentPadding = PaddingValues(vertical = 8.dp, horizontal = 10.dp)
) { ) {
googleLogin() googleLogin()
} }
}
//登录tab //登录tab
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
ActionButton( Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.login_upper), text = stringResource(R.string.login_upper),
color = AppColors.text, color = AppColors.text.copy(alpha = 0.5f),
) { fontSize = 16.sp,
navController.navigate( textAlign = TextAlign.Center,
NavigationRoute.UserAuth.route, modifier = Modifier
) .fillMaxWidth()
} .noRippleClickable {
navController.navigate(
// 游客登录按钮 NavigationRoute.UserAuth.route,
Spacer(modifier = Modifier.height(16.dp)) )
ActionButton( }
modifier = Modifier.fillMaxWidth(), )
text = "游客模式", // // 游客登录按钮
color = AppColors.text.copy(alpha = 0.7f), // Spacer(modifier = Modifier.height(16.dp))
) { // ActionButton(
guestLogin() // modifier = Modifier.fillMaxWidth(),
} // text = "游客模式",
// color = AppColors.text.copy(alpha = 0.7f),
// ) {
// guestLogin()
// }
Spacer(modifier = Modifier.height(70.dp)) Spacer(modifier = Modifier.height(70.dp))
} }
} }

View File

@@ -22,6 +22,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -60,7 +62,7 @@ fun UserAuthScreen() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var email by remember { mutableStateOf("") } var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(false) } var rememberMe by remember { mutableStateOf(true) }
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
val captchaService: CaptchaService = CaptchaServiceImpl() val captchaService: CaptchaService = CaptchaServiceImpl()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -70,8 +72,17 @@ fun UserAuthScreen() {
var passwordError by remember { mutableStateOf<String?>(null) } var passwordError by remember { mutableStateOf<String?>(null) }
var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) } var captchaInfo by remember { mutableStateOf<CaptchaInfo?>(null) }
fun validateForm(): Boolean { fun validateForm(): Boolean {
emailError = // 如果密码为空,先检查邮箱格式
if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null if (password.isEmpty()) {
emailError = when {
email.isEmpty() -> context.getString(R.string.text_error_email_required)
!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() ->
context.getString(R.string.text_error_email_format)
else -> null
}
} else {
emailError = if (email.isEmpty()) context.getString(R.string.text_error_email_required) else null
}
// 使用通用密码校验器 // 使用通用密码校验器
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context) val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
@@ -299,32 +310,38 @@ fun UserAuthScreen() {
ActionButton( ActionButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.lets_ride_upper), text = stringResource(R.string.lets_ride_upper),
backgroundColor = AppColors.main, backgroundBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x777c68ef),
Color(0x777bd8f8)
)
),
color = AppColors.mainText, color = AppColors.mainText,
) { ) {
onLogin() onLogin()
} }
if (AppState.enableGoogleLogin) { // if (AppState.enableGoogleLogin) {
Spacer(modifier = Modifier.height(16.dp)) // Spacer(modifier = Modifier.height(16.dp))
Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText) // Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
Spacer(modifier = Modifier.height(16.dp)) // Spacer(modifier = Modifier.height(16.dp))
ActionButton( // ActionButton(
modifier = Modifier.fillMaxWidth(), // modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_in_with_google), // text = stringResource(R.string.sign_in_with_google),
color = AppColors.text, // color = AppColors.text,
leading = { // leading = {
Image( // Image(
painter = painterResource(id = R.drawable.rider_pro_google), // painter = painterResource(id = R.drawable.rider_pro_google),
contentDescription = "Google", // contentDescription = "Google",
modifier = Modifier.size(36.dp) // modifier = Modifier.size(36.dp)
) // )
}, // },
expandText = true, // expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp) // contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
) { // ) {
googleLogin() // googleLogin()
} // }
} // }
} }

View File

@@ -0,0 +1,64 @@
package com.aiosman.ravenow.ui.network
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
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.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.R
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -0,0 +1,144 @@
package com.aiosman.ravenow.ui.notification
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.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.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
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.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.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.follower.FollowerNoticeScreen
import com.aiosman.ravenow.ui.like.LikeNoticeScreen
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NotificationScreen() {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { 3 })
val Debouncer = rememberDebouncer()
Column(
modifier = Modifier
.fillMaxSize()
.background(color = AppColors.background)
) {
StatusBarSpacer()
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
Debouncer {
navController.popBackStack()
}
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.group_info_notice_setting),
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
TabItem(
text = stringResource(R.string.like),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.followers_upper),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.comment).uppercase(),
isSelected = pagerState.currentPage == 2,
onClick = {
scope.launch {
pagerState.animateScrollToPage(2)
}
}
)
}
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { page ->
when (page) {
0 -> LikeNoticeScreen()
1 -> FollowerNoticeScreen()
2 -> CommentNoticeScreen()
}
}
}
}

View File

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

View File

@@ -799,7 +799,7 @@ fun CommentContent(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Image( Image(
painter = painterResource(id = R.mipmap.qs_plq_qs_img), painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(181.dp) modifier = Modifier.size(181.dp)
) )
@@ -917,7 +917,20 @@ fun Header(
Text( Text(
text = nickname ?: "", text = nickname ?: "",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.debouncedClickable(debounceTime = 1000L) {
userId?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
}
}
},
color = AppColors.text, color = AppColors.text,
fontSize = 17.sp fontSize = 17.sp
) )
@@ -1196,7 +1209,7 @@ fun PostImageView(
) )
} }
// Navigation and Indicator container // 图片导航控件
if (images.size > 1) { if (images.size > 1) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1347,76 +1360,56 @@ fun CommentItem(
} }
) {} ) {}
) { ) {
Row { Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text( Text(
text = commentEntity.name, text = commentEntity.name,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 11.sp, fontSize = 11.sp,
color = AppColors.text color = AppColors.text
) )
Spacer(modifier = Modifier.width(8.dp)) Column(
Text( horizontalAlignment = Alignment.End
text = commentEntity.date.timeAgo(context), ) {
fontSize = 11.sp, AnimatedLikeIcon(
color = Color.Gray liked = commentEntity.liked,
) onClick = {
} // 检查游客模式,如果是游客则跳转登录
Row (modifier = Modifier.padding(top = 4.dp)){ if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
if (isChild) { debouncedNavigation {
val annotatedText = buildAnnotatedString { navController.navigate(NavigationRoute.Login.route)
if (commentEntity.replyUserId != null) {
pushStringAnnotation(
tag = "replyUser",
annotation = commentEntity.replyUserId.toString()
)
withStyle(
style = SpanStyle(
fontWeight = FontWeight.W600,
color = Color(0xFF6F94AE)
)
) {
append("@${commentEntity.replyUserNickname}")
}
pop()
}
append(" ${commentEntity.comment}")
}
Box {
CustomClickableText(
text = annotatedText,
onClick = { offset ->
annotatedText.getStringAnnotations(
tag = "replyUser",
start = offset,
end = offset
).firstOrNull()?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
it.item
)
)
}
} }
}, } else {
style = TextStyle(fontSize = 14.sp, color = AppColors.text), onLike(commentEntity)
onLongPress = { }
onLongClick(commentEntity) },
}, modifier = Modifier.size(16.dp)
) )
}
} else {
Text( Text(
text = commentEntity.comment, text = commentEntity.likes.toString(),
fontSize = 13.sp, fontSize = 12.sp,
maxLines = Int.MAX_VALUE, fontWeight = FontWeight.Bold,
softWrap = true,
lineHeight = 20.sp,
color = AppColors.text, color = AppColors.text,
modifier = Modifier.combinedClickable( modifier = Modifier.padding(top = 4.dp,end = 4.dp)
)
}
}
Text(
text = commentEntity.comment,
fontSize = 13.sp,
maxLines = Int.MAX_VALUE,
softWrap = true,
lineHeight = 20.sp,
color = AppColors.text,
modifier = Modifier
.fillMaxWidth()
.padding(end = 50.dp)
.padding(top = 0.dp)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null, indication = null,
onLongClick = { onLongClick = {
@@ -1427,48 +1420,21 @@ fun CommentItem(
) { ) {
} }
)
}
}
Row (modifier = Modifier.padding(top = 12.dp),
verticalAlignment = Alignment.CenterVertically,){
AnimatedLikeIcon(
liked = commentEntity.liked,
onClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
onLike(commentEntity)
}
},
modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp))
Text(
text = commentEntity.likes.toString(),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
Text(
text = stringResource(R.string.like),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = AppColors.nonActiveText,
)
Spacer(modifier = Modifier.width(27.dp))
Icon(
painter = painterResource(id = R.drawable.rider_pro_comment), Row (
contentDescription = "", modifier = Modifier.padding(top = 12.dp),
modifier = Modifier.size(16.dp), verticalAlignment = Alignment.CenterVertically,
tint = AppColors.nonActiveText ){
) Text(
Spacer(modifier = Modifier.width(4.dp)) text = commentEntity.date.timeAgo(context),
fontSize = 12.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = stringResource(R.string.reply), text = stringResource(R.string.reply),
fontSize = 12.sp, fontSize = 12.sp,
@@ -1541,6 +1507,7 @@ fun CommentItem(
} }
} }
@Composable @Composable
fun PostBottomBar( fun PostBottomBar(
onCreateCommentClick: () -> Unit = {}, onCreateCommentClick: () -> Unit = {},
@@ -1607,6 +1574,24 @@ fun PostBottomBar(
} }
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
AnimatedFavouriteIcon(
isFavourite = momentEntity?.isFavorite == true,
onClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
onFavoriteClick()
}
},
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
Spacer(modifier = Modifier.width(16.dp))
AnimatedLikeIcon( AnimatedLikeIcon(
liked = momentEntity?.liked == true, liked = momentEntity?.liked == true,
onClick = { onClick = {
@@ -1623,24 +1608,6 @@ fun PostBottomBar(
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.likeCount.toString(), color = AppColors.text) Text(text = momentEntity?.likeCount.toString(), color = AppColors.text)
Spacer(modifier = Modifier.width(16.dp))
AnimatedFavouriteIcon(
isFavourite = momentEntity?.isFavorite == true,
onClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
} else {
onFavoriteClick()
}
},
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(text = momentEntity?.favoriteCount.toString(), color = AppColors.text)
} }
BottomNavigationPlaceholder( BottomNavigationPlaceholder(
color = AppColors.background color = AppColors.background

View File

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

View File

@@ -1,20 +1,19 @@
package com.aiosman.ravenow.ui.splash package com.aiosman.ravenow.ui.splash
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.material.Scaffold
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -22,38 +21,36 @@ import com.aiosman.ravenow.R
@Composable @Composable
fun SplashScreen() { fun SplashScreen() {
Scaffold { Box(
it modifier = Modifier.fillMaxSize()
Box( ) {
modifier = Modifier.fillMaxSize() // 居中的图标
Image(
painter = painterResource(id = R.mipmap.kp_logo_img),
contentDescription = "App Logo",
modifier = Modifier
.align(Alignment.Center)
.size(120.dp)
)
// 底部文字
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 80.dp)
) { ) {
// to bottom Image(
Box( painterResource(id = R.mipmap.kp_p_img),
contentAlignment = Alignment.TopCenter, contentDescription = "",
modifier = Modifier.padding(top = 211.dp) modifier = Modifier.size(85.dp, 25.dp)
) { )
Column( Spacer(modifier = Modifier.padding(top = 16.dp))
horizontalAlignment = Alignment.CenterHorizontally, Text(
modifier = Modifier.fillMaxWidth() stringResource(R.string.splash_title),
) { fontSize = 13.sp
Image( )
painter = painterResource(id = R.mipmap.rider_pro_logo),
contentDescription = "Rave Now",
modifier = Modifier
.width(108.dp)
.height(45.dp)
)
Spacer(modifier = Modifier.height(32.dp))
Text(
"Rave Now".uppercase(),
fontSize = 28.sp,
fontWeight = FontWeight.Bold
)
Text("Your Night Starts Here".uppercase(), fontSize = 20.sp, fontWeight = FontWeight.W700)
}
}
} }
} }
} }

View File

@@ -63,6 +63,23 @@ object Utils {
return Locale.getDefault().language return Locale.getDefault().language
} }
/**
* 获取完整的语言标记,如 "zh-CN", "en-US"
* 优先使用完整的 BCP-47 语言标记,提升与后端 translations 键的匹配率
*/
fun getPreferredLanguageTag(): String {
val locale = Locale.getDefault()
val language = locale.language
val country = locale.country
// 如果有国家/地区代码,返回完整的语言标记
return if (country.isNotEmpty()) {
"$language-$country"
} else {
language
}
}
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File { fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
val inputStream = context.contentResolver.openInputStream(uri) val inputStream = context.contentResolver.openInputStream(uri)
val originalBitmap = BitmapFactory.decodeStream(inputStream) val originalBitmap = BitmapFactory.decodeStream(inputStream)

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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