Compare commits
5 Commits
main
...
feat/pr-20
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a19d4c60e | |||
| a057f7f7fd | |||
| f95c0204ee | |||
| c5156a5f6c | |||
| de85f35c45 |
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -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-11-11T06:03:31.167121900Z">
|
<DropdownSelection timestamp="2025-11-05T12:24:27.034893100Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=f800b364" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=c328a150" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -13,6 +13,7 @@
|
|||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="2.2.21" />
|
<option name="version" value="1.9.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
alias(libs.plugins.jetbrains.kotlin.android)
|
||||||
alias(libs.plugins.compose.compiler)
|
|
||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
id("com.google.firebase.crashlytics")
|
id("com.google.firebase.crashlytics")
|
||||||
id("com.google.firebase.firebase-perf")
|
id("com.google.firebase.firebase-perf")
|
||||||
id("org.jetbrains.kotlin.kapt")
|
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
android {
|
android {
|
||||||
namespace = "com.aiosman.ravenow"
|
namespace = "com.aiosman.ravenow"
|
||||||
compileSdk = 35
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.aiosman.ravenow"
|
applicationId = "com.aiosman.ravenow"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 34
|
||||||
versionCode = 1000019
|
versionCode = 1000019
|
||||||
versionName = "1.0.000.19"
|
versionName = "1.0.000.19"
|
||||||
|
|
||||||
@@ -47,16 +44,19 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.3"
|
||||||
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
@@ -97,13 +97,11 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
implementation(libs.androidx.animation)
|
implementation(libs.androidx.animation)
|
||||||
implementation(libs.coil)
|
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.coil.network.okhttp)
|
implementation(libs.coil)
|
||||||
implementation(libs.play.services.auth)
|
implementation(libs.play.services.auth)
|
||||||
implementation(libs.kotlin.faker)
|
implementation(libs.kotlin.faker)
|
||||||
implementation(libs.androidx.material)
|
implementation(libs.androidx.material)
|
||||||
implementation(libs.androidx.material.icons.extended)
|
|
||||||
implementation(libs.zoomable)
|
implementation(libs.zoomable)
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
implementation(libs.converter.gson)
|
implementation(libs.converter.gson)
|
||||||
@@ -129,16 +127,5 @@ dependencies {
|
|||||||
implementation (libs.eventbus)
|
implementation (libs.eventbus)
|
||||||
implementation(libs.lottie)
|
implementation(libs.lottie)
|
||||||
|
|
||||||
// CameraX + ML Kit(版本在 libs.versions.toml)
|
|
||||||
implementation(libs.androidx.camera.camera2)
|
|
||||||
implementation(libs.androidx.camera.lifecycle)
|
|
||||||
implementation(libs.androidx.camera.view)
|
|
||||||
implementation(libs.mlkit.barcode.scanning)
|
|
||||||
|
|
||||||
// Room 持久化
|
|
||||||
implementation(libs.androidx.room.runtime)
|
|
||||||
implementation(libs.androidx.room.ktx)
|
|
||||||
ksp(libs.androidx.room.compiler)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".RaveNowApplication"
|
android:name=".RaveNowApplication"
|
||||||
@@ -53,8 +51,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize">
|
||||||
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,6 @@ import com.aiosman.ravenow.data.AccountService
|
|||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.ravenow.data.DictService
|
import com.aiosman.ravenow.data.DictService
|
||||||
import com.aiosman.ravenow.data.DictServiceImpl
|
import com.aiosman.ravenow.data.DictServiceImpl
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel
|
||||||
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeViewModel
|
||||||
@@ -32,7 +31,6 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
|
|||||||
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
||||||
import com.aiosman.ravenow.utils.Utils
|
import com.aiosman.ravenow.utils.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import com.aiosman.ravenow.im.OpenIMManager
|
import com.aiosman.ravenow.im.OpenIMManager
|
||||||
import io.openim.android.sdk.OpenIMClient
|
import io.openim.android.sdk.OpenIMClient
|
||||||
@@ -52,7 +50,7 @@ object AppState {
|
|||||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||||
// 如果是游客模式,使用简化的初始化流程
|
// 如果是游客模式,使用简化的初始化流程
|
||||||
if (AppStore.isGuest) {
|
if (AppStore.isGuest) {
|
||||||
initWithGuestAccount(scope)
|
initWithGuestAccount()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,58 +81,18 @@ object AppState {
|
|||||||
// 注册 JPush
|
// 注册 JPush
|
||||||
Messaging.registerDevice(scope, context)
|
Messaging.registerDevice(scope, context)
|
||||||
initChat(context)
|
initChat(context)
|
||||||
|
|
||||||
// 设置当前用户并刷新积分信息(完成登录态初始化后)
|
|
||||||
PointService.setCurrentUser(UserId)
|
|
||||||
try {
|
|
||||||
PointService.refreshMyPointsBalance()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AppState", "刷新积分失败: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 并行加载积分规则和房间规则配置(不阻塞主流程)
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
PointService.refreshPointsRules()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AppState", "加载积分规则失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
PointService.refreshRoomMaxMembers()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AppState", "加载房间规则失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游客模式的简化初始化
|
* 游客模式的简化初始化
|
||||||
*/
|
*/
|
||||||
private fun initWithGuestAccount(scope: CoroutineScope) {
|
private fun initWithGuestAccount() {
|
||||||
// 游客模式下,不初始化推送和TRTC
|
// 游客模式下,不初始化推送和TRTC
|
||||||
// 设置默认的用户信息
|
// 设置默认的用户信息
|
||||||
UserId = 0
|
UserId = 0
|
||||||
profile = null
|
profile = null
|
||||||
enableChat = false
|
enableChat = false
|
||||||
Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
|
Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
|
||||||
|
|
||||||
// 游客模式下也加载规则配置(用于查看费用信息)
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
PointService.refreshPointsRules()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AppState", "加载积分规则失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
PointService.refreshRoomMaxMembers()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AppState", "加载房间规则失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initChat(context: Context){
|
private suspend fun initChat(context: Context){
|
||||||
@@ -270,8 +228,6 @@ object AppState {
|
|||||||
AgentViewModel.ResetModel()
|
AgentViewModel.ResetModel()
|
||||||
MineAgentViewModel.ResetModel()
|
MineAgentViewModel.ResetModel()
|
||||||
UserId = null
|
UserId = null
|
||||||
// 清空积分全局状态,避免用户切换串号
|
|
||||||
PointService.clear()
|
|
||||||
|
|
||||||
// 清除游客状态
|
// 清除游客状态
|
||||||
AppStore.isGuest = false
|
AppStore.isGuest = false
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class DarkThemeColors : AppThemeData(
|
|||||||
chatActionColor = Color(0xFF3D3D3D),
|
chatActionColor = Color(0xFF3D3D3D),
|
||||||
brandColorsColor = Color(0xffD80264),
|
brandColorsColor = Color(0xffD80264),
|
||||||
tabSelectedBackground = Color(0xffffffff),
|
tabSelectedBackground = Color(0xffffffff),
|
||||||
tabUnselectedBackground = Color(0xFF1C1C1C),
|
tabUnselectedBackground = Color(0x2E7C7480),
|
||||||
tabSelectedText = Color(0xff000000),
|
tabSelectedText = Color(0xff000000),
|
||||||
tabUnselectedText = Color(0xffffffff),
|
tabUnselectedText = Color(0xffffffff),
|
||||||
bubbleBackground = Color(0xff2d2c2e),
|
bubbleBackground = Color(0xff2d2c2e),
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.ImageLoader
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil3.compose.rememberAsyncImagePainter
|
import coil.request.ImageRequest
|
||||||
import coil3.disk.DiskCache
|
import coil.ImageLoader
|
||||||
import coil3.memory.MemoryCache
|
import coil.disk.DiskCache
|
||||||
import coil3.request.ImageRequest
|
import coil.memory.MemoryCache
|
||||||
import coil3.request.crossfade
|
|
||||||
import okio.Path.Companion.toPath
|
|
||||||
|
|
||||||
data class ImageItem(val url: String)
|
data class ImageItem(val url: String)
|
||||||
|
|
||||||
@@ -55,15 +53,14 @@ fun ImageItem(item: ImageItem, imageLoader: ImageLoader, context: Context) { //
|
|||||||
fun getImageLoader(context: Context): ImageLoader {
|
fun getImageLoader(context: Context): ImageLoader {
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.memoryCache {
|
.memoryCache {
|
||||||
MemoryCache.Builder()
|
MemoryCache.Builder(context)
|
||||||
.maxSizePercent(context,0.25) // 设置内存缓存大小为可用内存的 25%
|
.maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.diskCache {
|
.diskCache {
|
||||||
val cacheDir = context.cacheDir.resolve("image_cache")
|
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
.directory(cacheDir.absolutePath.toPath())
|
.directory(context.cacheDir.resolve("image_cache"))
|
||||||
.maxSizeBytes(250L * 1024 * 1024) // 250MB
|
.maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -23,8 +22,6 @@ import androidx.compose.animation.SharedTransitionScope
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
@@ -40,7 +37,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
|
|||||||
import com.aiosman.ravenow.ui.dialogs.CheckUpdateDialog
|
import com.aiosman.ravenow.ui.dialogs.CheckUpdateDialog
|
||||||
import com.aiosman.ravenow.ui.navigateToPost
|
import com.aiosman.ravenow.ui.navigateToPost
|
||||||
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
import com.aiosman.ravenow.ui.post.NewPostViewModel
|
||||||
import com.aiosman.ravenow.ui.points.PointsBottomSheetHost
|
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.analytics.analytics
|
import com.google.firebase.analytics.analytics
|
||||||
@@ -60,25 +56,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val scope = CoroutineScope(Dispatchers.Main)
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
val context = this
|
val context = this
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context) {
|
|
||||||
// 禁用字体缩放,固定字体大小为系统默认大小
|
|
||||||
val configuration = Configuration(newBase.resources.configuration)
|
|
||||||
configuration.fontScale = 1.0f
|
|
||||||
val context = newBase.createConfigurationContext(configuration)
|
|
||||||
super.attachBaseContext(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
// 确保配置变化时字体缩放保持为 1.0
|
|
||||||
val config = Configuration(newConfig)
|
|
||||||
config.fontScale = 1.0f
|
|
||||||
super.onConfigurationChanged(config)
|
|
||||||
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
if (AppState.darkMode != isNightMode) {
|
|
||||||
syncDarkModeWithSystem(isNightMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求通知权限
|
// 请求通知权限
|
||||||
private val requestPermissionLauncher = registerForActivityResult(
|
private val requestPermissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission(),
|
ActivityResultContracts.RequestPermission(),
|
||||||
@@ -133,7 +110,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
JPushInterface.init(this)
|
JPushInterface.init(this)
|
||||||
|
|
||||||
updateWindowBackground(AppState.darkMode)
|
if (AppState.darkMode) {
|
||||||
|
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -148,15 +127,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
// 强制字体缩放为 1.0 - 通过覆盖 Density 来实现
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val fixedDensity = remember {
|
|
||||||
androidx.compose.ui.unit.Density(
|
|
||||||
density = density.density,
|
|
||||||
fontScale = 1.0f
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showSplash by remember { mutableStateOf(true) }
|
var showSplash by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -168,12 +138,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
SplashScreen()
|
SplashScreen()
|
||||||
} else {
|
} else {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAppTheme provides AppState.appTheme,
|
LocalAppTheme provides AppState.appTheme
|
||||||
LocalDensity provides fixedDensity
|
|
||||||
) {
|
) {
|
||||||
CheckUpdateDialog()
|
CheckUpdateDialog()
|
||||||
// 全局挂载积分底部弹窗 Host
|
|
||||||
PointsBottomSheetHost()
|
|
||||||
Navigation(startDestination) { navController ->
|
Navigation(startDestination) { navController ->
|
||||||
|
|
||||||
// 处理带有 postId 的通知点击
|
// 处理带有 postId 的通知点击
|
||||||
@@ -271,22 +238,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
|
|
||||||
AppState.darkMode = isNightMode
|
|
||||||
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
|
|
||||||
AppStore.saveDarkMode(isNightMode)
|
|
||||||
updateWindowBackground(isNightMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWindowBackground(isDarkMode: Boolean) {
|
|
||||||
window.decorView.setBackgroundColor(
|
|
||||||
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavHostController> {
|
val LocalNavController = compositionLocalOf<NavHostController> {
|
||||||
error("NavController not provided")
|
error("NavController not provided")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
import com.google.firebase.perf.FirebasePerformance
|
import com.google.firebase.perf.FirebasePerformance
|
||||||
@@ -12,14 +11,6 @@ import com.google.firebase.perf.FirebasePerformance
|
|||||||
*/
|
*/
|
||||||
class RaveNowApplication : Application() {
|
class RaveNowApplication : Application() {
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
// 禁用字体缩放,固定字体大小为系统默认大小
|
|
||||||
val configuration = Configuration(base.resources.configuration)
|
|
||||||
configuration.fontScale = 1.0f
|
|
||||||
val context = base.createConfigurationContext(configuration)
|
|
||||||
super.attachBaseContext(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
|||||||
@@ -64,15 +64,6 @@ data class AccountProfile(
|
|||||||
val aiAccount: Boolean,
|
val aiAccount: Boolean,
|
||||||
|
|
||||||
val chatAIId: String,
|
val chatAIId: String,
|
||||||
|
|
||||||
// AI角色背景图
|
|
||||||
val aiRoleAvatar: String? = null,
|
|
||||||
val aiRoleAvatarMedium: String? = null,
|
|
||||||
val aiRoleAvatarLarge: String? = null,
|
|
||||||
|
|
||||||
// 创建者信息(仅AI账号有)
|
|
||||||
@SerializedName("creatorProfile")
|
|
||||||
val creatorProfile: com.aiosman.ravenow.data.CreatorProfile? = null,
|
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* 转换为Entity
|
* 转换为Entity
|
||||||
@@ -98,17 +89,7 @@ data class AccountProfile(
|
|||||||
chatToken = openImToken,
|
chatToken = openImToken,
|
||||||
aiAccount = aiAccount,
|
aiAccount = aiAccount,
|
||||||
rawAvatar = avatar,
|
rawAvatar = avatar,
|
||||||
chatAIId = chatAIId,
|
chatAIId = chatAIId
|
||||||
aiRoleAvatar = aiRoleAvatar?.let {
|
|
||||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
|
||||||
},
|
|
||||||
aiRoleAvatarMedium = aiRoleAvatarMedium?.let {
|
|
||||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
|
||||||
},
|
|
||||||
aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
|
|
||||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
|
||||||
},
|
|
||||||
creatorProfile = creatorProfile?.toCreatorProfileEntity()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,16 +417,15 @@ interface AccountService {
|
|||||||
* @param page 页码
|
* @param page 页码
|
||||||
* @param pageSize 每页数量
|
* @param pageSize 每页数量
|
||||||
*/
|
*/
|
||||||
suspend fun getAgent(page: Int, pageSize: Int, excludeRoomId: Int? = null, title: String? = null, desc: String? = null): retrofit2.Response<DataContainer<ListContainer<Agent>>>
|
suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建群聊
|
* 创建群聊
|
||||||
* @param name 群聊名称
|
* @param name 群聊名称
|
||||||
* @param userIds 用户ID列表
|
* @param userIds 用户ID列表
|
||||||
* @param promptIds AI智能体ID列表
|
* @param promptIds AI智能体ID列表
|
||||||
* @param roomId 房间ID,如果提供则添加成员到现有群聊,否则创建新群聊
|
|
||||||
*/
|
*/
|
||||||
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>, roomId: Int? = null): retrofit2.Response<DataContainer<Unit>>
|
suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountServiceImpl : AccountService {
|
class AccountServiceImpl : AccountService {
|
||||||
@@ -545,13 +525,7 @@ class AccountServiceImpl : AccountService {
|
|||||||
val bannerField: MultipartBody.Part? = banner?.let {
|
val bannerField: MultipartBody.Part? = banner?.let {
|
||||||
createMultipartBody(it.file, it.filename, "banner")
|
createMultipartBody(it.file, it.filename, "banner")
|
||||||
}
|
}
|
||||||
val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
|
||||||
if (!resp.isSuccessful) {
|
|
||||||
parseErrorResponse(resp.errorBody())?.let {
|
|
||||||
throw it.toServiceException()
|
|
||||||
}
|
|
||||||
throw ServiceException("Failed to update profile")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registerUserWithPassword(loginName: String, password: String) {
|
override suspend fun registerUserWithPassword(loginName: String, password: String) {
|
||||||
@@ -656,15 +630,15 @@ class AccountServiceImpl : AccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAgent(page: Int, pageSize: Int, excludeRoomId: Int?, title: String?, desc: String?): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
|
override suspend fun getAgent(page: Int, pageSize: Int): retrofit2.Response<DataContainer<ListContainer<Agent>>> {
|
||||||
return ApiClient.api.getAgent(page, pageSize, excludeRoomId = excludeRoomId, title = title, desc = desc)
|
return ApiClient.api.getAgent(page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>, roomId: Int?): retrofit2.Response<DataContainer<Unit>> {
|
override suspend fun createGroupChat(name: String, userIds: List<String>, promptIds: List<String>): retrofit2.Response<DataContainer<Unit>> {
|
||||||
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
|
val requestBody = com.aiosman.ravenow.data.api.CreateGroupChatRequestBody(
|
||||||
name = name,
|
name = name,
|
||||||
userIds = userIds,
|
userIds = userIds,
|
||||||
promptIds = promptIds,
|
promptIds = promptIds
|
||||||
)
|
)
|
||||||
return ApiClient.api.createGroupChat(requestBody)
|
return ApiClient.api.createGroupChat(requestBody)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ data class Profile(
|
|||||||
@SerializedName("nickname")
|
@SerializedName("nickname")
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
@SerializedName("trtcUserId")
|
@SerializedName("trtcUserId")
|
||||||
val trtcUserId: String? = null,
|
val trtcUserId: String,
|
||||||
@SerializedName("username")
|
@SerializedName("username")
|
||||||
val username: String
|
val username: String
|
||||||
){
|
){
|
||||||
@@ -85,7 +85,7 @@ data class Profile(
|
|||||||
avatar = "${ApiClient.BASE_SERVER}$avatar",
|
avatar = "${ApiClient.BASE_SERVER}$avatar",
|
||||||
bio = bio,
|
bio = bio,
|
||||||
banner = "${ApiClient.BASE_SERVER}$banner",
|
banner = "${ApiClient.BASE_SERVER}$banner",
|
||||||
trtcUserId = trtcUserId ?: "",
|
trtcUserId = trtcUserId,
|
||||||
chatAIId = chatAIId,
|
chatAIId = chatAIId,
|
||||||
aiAccount = aiAccount
|
aiAccount = aiAccount
|
||||||
)
|
)
|
||||||
@@ -108,15 +108,6 @@ interface AgentService {
|
|||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity>?
|
): ListContainer<AgentEntity>?
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据标题关键字搜索智能体
|
|
||||||
*/
|
|
||||||
suspend fun searchAgentByTitle(
|
|
||||||
pageNumber: Int,
|
|
||||||
pageSize: Int = 20,
|
|
||||||
title: String
|
|
||||||
): ListContainer<AgentEntity>?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,16 +123,12 @@ data class Comment(
|
|||||||
fun toCommentEntity(): CommentEntity {
|
fun toCommentEntity(): CommentEntity {
|
||||||
return CommentEntity(
|
return CommentEntity(
|
||||||
id = id,
|
id = id,
|
||||||
name = user.nickName ?: "未知用户",
|
name = user.nickName,
|
||||||
comment = content,
|
comment = content,
|
||||||
date = ApiClient.dateFromApiString(createdAt),
|
date = ApiClient.dateFromApiString(createdAt),
|
||||||
likes = likeCount,
|
likes = likeCount,
|
||||||
postId = postId,
|
postId = postId,
|
||||||
avatar = if (user.avatar != null && user.avatar.isNotEmpty()) {
|
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
|
||||||
"${ApiClient.BASE_SERVER}${user.avatar}"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
author = user.id,
|
author = user.id,
|
||||||
liked = isLiked,
|
liked = isLiked,
|
||||||
unread = isUnread,
|
unread = isUnread,
|
||||||
|
|||||||
@@ -14,16 +14,6 @@ interface DictService {
|
|||||||
* 获取字典列表
|
* 获取字典列表
|
||||||
*/
|
*/
|
||||||
suspend fun getDistList(keys: List<String>): List<DictItem>
|
suspend fun getDistList(keys: List<String>): List<DictItem>
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取外部字典项
|
|
||||||
*/
|
|
||||||
suspend fun getOutsideDictByKey(key: String): DictItem
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取外部字典列表
|
|
||||||
*/
|
|
||||||
suspend fun getOutsideDistList(keys: List<String>): List<DictItem>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DictServiceImpl : DictService {
|
class DictServiceImpl : DictService {
|
||||||
@@ -36,13 +26,4 @@ class DictServiceImpl : DictService {
|
|||||||
val resp = ApiClient.api.getDicts(keys.joinToString(","))
|
val resp = ApiClient.api.getDicts(keys.joinToString(","))
|
||||||
return resp.body()?.list ?: throw Exception("failed to get dict list")
|
return resp.body()?.list ?: throw Exception("failed to get dict list")
|
||||||
}
|
}
|
||||||
override suspend fun getOutsideDictByKey(key: String): DictItem {
|
|
||||||
val resp = ApiClient.api.getOutsideDict(key)
|
|
||||||
return resp.body()?.data ?: throw Exception("failed to get outside dict")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getOutsideDistList(keys: List<String>): List<DictItem> {
|
|
||||||
val resp = ApiClient.api.getOutsideDicts(keys.joinToString(","))
|
|
||||||
return resp.body()?.list ?: throw Exception("failed to get outside dict list")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ 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
|
||||||
|
|
||||||
@@ -13,12 +12,8 @@ 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>? = null,
|
val images: List<Image>,
|
||||||
@SerializedName("videos")
|
|
||||||
val videos: List<Video>? = null,
|
|
||||||
@SerializedName("user")
|
@SerializedName("user")
|
||||||
val user: User,
|
val user: User,
|
||||||
@SerializedName("likeCount")
|
@SerializedName("likeCount")
|
||||||
@@ -29,12 +24,12 @@ data class Moment(
|
|||||||
val favoriteCount: Long,
|
val favoriteCount: Long,
|
||||||
@SerializedName("isFavorite")
|
@SerializedName("isFavorite")
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
@SerializedName("isCommented")
|
@SerializedName("shareCount")
|
||||||
val isCommented: Boolean,
|
val isCommented: Boolean,
|
||||||
@SerializedName("commentCount")
|
@SerializedName("commentCount")
|
||||||
val commentCount: Long,
|
val commentCount: Long,
|
||||||
@SerializedName("time")
|
@SerializedName("time")
|
||||||
val time: String?,
|
val time: String,
|
||||||
@SerializedName("isFollowed")
|
@SerializedName("isFollowed")
|
||||||
val isFollowed: Boolean,
|
val isFollowed: Boolean,
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
@@ -52,30 +47,14 @@ data class Moment(
|
|||||||
val newsLanguage: String? = null,
|
val newsLanguage: String? = null,
|
||||||
@SerializedName("newsContent")
|
@SerializedName("newsContent")
|
||||||
val newsContent: String? = null,
|
val newsContent: String? = null,
|
||||||
@SerializedName("hasFullText")
|
|
||||||
val hasFullText: Boolean = false,
|
|
||||||
@SerializedName("summary")
|
|
||||||
val summary: String? = null,
|
|
||||||
@SerializedName("publishedAt")
|
|
||||||
val publishedAt: String? = null,
|
|
||||||
@SerializedName("imageCached")
|
|
||||||
val imageCached: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
fun toMomentItem(): MomentEntity {
|
fun toMomentItem(): MomentEntity {
|
||||||
return MomentEntity(
|
return MomentEntity(
|
||||||
id = id.toInt(),
|
id = id.toInt(),
|
||||||
avatar = if (user.avatar != null && user.avatar.isNotEmpty()) {
|
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
|
||||||
"${ApiClient.BASE_SERVER}${user.avatar}"
|
nickname = user.nickName,
|
||||||
} else {
|
|
||||||
"" // 如果头像为空,使用空字符串
|
|
||||||
},
|
|
||||||
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
|
|
||||||
location = "Worldwide",
|
location = "Worldwide",
|
||||||
time = if (time != null && time.isNotEmpty()) {
|
time = ApiClient.dateFromApiString(time),
|
||||||
ApiClient.dateFromApiString(time)
|
|
||||||
} else {
|
|
||||||
java.util.Date() // 如果时间为空,使用当前时间作为默认值
|
|
||||||
},
|
|
||||||
followStatus = isFollowed,
|
followStatus = isFollowed,
|
||||||
momentTextContent = textContent,
|
momentTextContent = textContent,
|
||||||
momentPicture = R.drawable.default_moment_img,
|
momentPicture = R.drawable.default_moment_img,
|
||||||
@@ -83,46 +62,19 @@ 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(
|
||||||
id = it.id,
|
|
||||||
url = "${ApiClient.BASE_SERVER}${it.url}",
|
url = "${ApiClient.BASE_SERVER}${it.url}",
|
||||||
originalUrl = it.originalUrl,
|
|
||||||
directUrl = it.directUrl,
|
|
||||||
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
|
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
|
||||||
thumbnailDirectUrl = it.thumbnailDirectUrl,
|
id = it.id,
|
||||||
small = it.small,
|
|
||||||
smallDirectUrl = it.smallDirectUrl,
|
|
||||||
medium = it.medium,
|
|
||||||
mediumDirectUrl = it.mediumDirectUrl,
|
|
||||||
large = it.large,
|
|
||||||
largeDirectUrl = it.largeDirectUrl,
|
|
||||||
blurHash = it.blurHash,
|
blurHash = it.blurHash,
|
||||||
width = it.width,
|
width = it.width,
|
||||||
height = it.height
|
height = it.height
|
||||||
)
|
)
|
||||||
} ?: emptyList(),
|
},
|
||||||
authorId = user.id.toInt(),
|
authorId = user.id.toInt(),
|
||||||
liked = isLiked,
|
liked = isLiked,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
url = url,
|
|
||||||
videos = videos?.map {
|
|
||||||
MomentVideoEntity(
|
|
||||||
id = it.id,
|
|
||||||
url = "${ApiClient.BASE_SERVER}${it.url}",
|
|
||||||
originalUrl = it.originalUrl,
|
|
||||||
directUrl = it.directUrl,
|
|
||||||
thumbnailUrl = it.thumbnailUrl?.let { thumb -> "${ApiClient.BASE_SERVER}$thumb" },
|
|
||||||
thumbnailDirectUrl = it.thumbnailDirectUrl,
|
|
||||||
duration = it.duration,
|
|
||||||
width = it.width,
|
|
||||||
height = it.height,
|
|
||||||
size = it.size,
|
|
||||||
format = it.format,
|
|
||||||
bitrate = it.bitrate,
|
|
||||||
frameRate = it.frameRate
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
isNews = isNews,
|
isNews = isNews,
|
||||||
newsTitle = newsTitle ?: "",
|
newsTitle = newsTitle ?: "",
|
||||||
@@ -130,11 +82,7 @@ data class Moment(
|
|||||||
newsSource = newsSource ?: "",
|
newsSource = newsSource ?: "",
|
||||||
newsCategory = newsCategory ?: "",
|
newsCategory = newsCategory ?: "",
|
||||||
newsLanguage = newsLanguage ?: "",
|
newsLanguage = newsLanguage ?: "",
|
||||||
newsContent = newsContent ?: "",
|
newsContent = newsContent ?: ""
|
||||||
hasFullText = hasFullText,
|
|
||||||
summary = summary,
|
|
||||||
publishedAt = publishedAt,
|
|
||||||
imageCached = imageCached
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,26 +92,8 @@ 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")
|
||||||
@@ -172,68 +102,13 @@ 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(
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
package com.aiosman.ravenow.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.aiosman.ravenow.AppStore
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
|
||||||
import com.aiosman.ravenow.data.api.DictItem
|
|
||||||
import com.aiosman.ravenow.data.api.PointsBalance
|
|
||||||
import com.aiosman.ravenow.data.api.PointsChangeLogsResponse
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分服务
|
|
||||||
*
|
|
||||||
* 提供积分余额查询和积分变更日志查询功能
|
|
||||||
*/
|
|
||||||
object PointService {
|
|
||||||
|
|
||||||
// ========== 定价表(字典 points_rules)相关 ==========
|
|
||||||
private val dictService: DictService = DictServiceImpl()
|
|
||||||
private val gson = Gson()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分规则key常量
|
|
||||||
* 对应积分规则JSON中的key值
|
|
||||||
*/
|
|
||||||
object PointsRuleKey {
|
|
||||||
// 获得积分类型(add)
|
|
||||||
/** 每日登录奖励 */
|
|
||||||
const val DAILY_LOGIN = "daily_login"
|
|
||||||
/** 用户注册奖励 */
|
|
||||||
const val USER_REGISTER = "user_register"
|
|
||||||
|
|
||||||
// 消费积分类型(sub)
|
|
||||||
/** 添加Agent记忆 */
|
|
||||||
const val ADD_AGENT_MEMORY = "add_agent_memory"
|
|
||||||
/** 增加房间容量 */
|
|
||||||
const val ADD_ROOM_CAP = "add_room_cap"
|
|
||||||
/** 创建房间 */
|
|
||||||
const val CREATE_ROOM = "create_room"
|
|
||||||
/** 创建定时事件 */
|
|
||||||
const val CREATE_SCHEDULE_EVENT = "create_schedule_event"
|
|
||||||
/** 房间私密模式 */
|
|
||||||
const val ROOM_PRIVATE = "room_private"
|
|
||||||
/** Agent私密模式 */
|
|
||||||
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
|
|
||||||
/** 自定义聊天背景 */
|
|
||||||
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
|
|
||||||
/** 房间记忆添加 */
|
|
||||||
const val SPEND_ROOM_MEMORY = "spend_room_memory"
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class RuleAmount {
|
|
||||||
data class Fixed(val value: Int) : RuleAmount()
|
|
||||||
data class Range(val min: Int, val max: Int) : RuleAmount()
|
|
||||||
}
|
|
||||||
data class PointsRules(
|
|
||||||
val add: Map<String, RuleAmount>,
|
|
||||||
val sub: Map<String, RuleAmount>
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _pointsRules = MutableStateFlow<PointsRules?>(null)
|
|
||||||
val pointsRules: StateFlow<PointsRules?> = _pointsRules.asStateFlow()
|
|
||||||
|
|
||||||
suspend fun refreshPointsRules(key: String = "points_rules") {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val dict = dictService.getDictByKey(key)
|
|
||||||
val rules = parsePointsRules(dict)
|
|
||||||
_pointsRules.value = rules
|
|
||||||
} catch (_: Exception) {
|
|
||||||
_pointsRules.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 群聊人数限制(字典 points-rule)相关 ==========
|
|
||||||
/**
|
|
||||||
* 群聊人数限制配置
|
|
||||||
* @param defaultMaxTotal 初始最大人数(默认值)
|
|
||||||
* @param maxTotal 最大人数(上限)
|
|
||||||
*/
|
|
||||||
data class RoomMaxMembers(
|
|
||||||
val defaultMaxTotal: Int,
|
|
||||||
val maxTotal: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _roomMaxMembers = MutableStateFlow<RoomMaxMembers?>(null)
|
|
||||||
val roomMaxMembers: StateFlow<RoomMaxMembers?> = _roomMaxMembers.asStateFlow()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新群聊人数限制配置(从外部字典表加载 points-rule)
|
|
||||||
* 加载时机与 refreshPointsRules 一致
|
|
||||||
*/
|
|
||||||
suspend fun refreshRoomMaxMembers(key: String = "points-rule") {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val dict = dictService.getOutsideDictByKey(key)
|
|
||||||
val config = parseRoomMaxMembers(dict)
|
|
||||||
_roomMaxMembers.value = config
|
|
||||||
} catch (_: Exception) {
|
|
||||||
_roomMaxMembers.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析群聊人数限制配置
|
|
||||||
* 解析格式:{"room":{"default":{"max-total":5},"max":{"max-total":200}}}
|
|
||||||
*/
|
|
||||||
private fun parseRoomMaxMembers(dict: DictItem): RoomMaxMembers? {
|
|
||||||
val raw = dict.value
|
|
||||||
val jsonStr = when (raw) {
|
|
||||||
is String -> raw
|
|
||||||
else -> gson.toJson(raw)
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
val root = JsonParser.parseString(jsonStr).asJsonObject
|
|
||||||
val roomObj = root.getAsJsonObject("room")
|
|
||||||
val defaultObj = roomObj?.getAsJsonObject("default")
|
|
||||||
val maxObj = roomObj?.getAsJsonObject("max")
|
|
||||||
|
|
||||||
val defaultMaxTotal = defaultObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 5
|
|
||||||
val maxTotal = maxObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 200
|
|
||||||
|
|
||||||
RoomMaxMembers(
|
|
||||||
defaultMaxTotal = defaultMaxTotal,
|
|
||||||
maxTotal = maxTotal
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parsePointsRules(dict: DictItem): PointsRules? {
|
|
||||||
val raw = dict.value
|
|
||||||
val jsonStr = when (raw) {
|
|
||||||
is String -> raw
|
|
||||||
else -> gson.toJson(raw)
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
val root = JsonParser.parseString(jsonStr).asJsonObject
|
|
||||||
fun parseBlock(block: JsonObject?): Map<String, RuleAmount> {
|
|
||||||
if (block == null) return emptyMap()
|
|
||||||
return block.entrySet().associate { entry ->
|
|
||||||
val key = entry.key
|
|
||||||
val v: JsonElement = entry.value
|
|
||||||
val amount: RuleAmount = when {
|
|
||||||
v.isJsonPrimitive && v.asJsonPrimitive.isNumber -> {
|
|
||||||
RuleAmount.Fixed(v.asInt)
|
|
||||||
}
|
|
||||||
v.isJsonPrimitive && v.asJsonPrimitive.isString -> {
|
|
||||||
val s = v.asString.trim()
|
|
||||||
if (s.contains("-")) {
|
|
||||||
val parts = s.split("-")
|
|
||||||
.map { it.trim() }
|
|
||||||
.filter { it.isNotEmpty() }
|
|
||||||
val min = parts.getOrNull(0)?.toIntOrNull()
|
|
||||||
val max = parts.getOrNull(1)?.toIntOrNull()
|
|
||||||
when {
|
|
||||||
min != null && max != null -> RuleAmount.Range(min, max)
|
|
||||||
min != null -> RuleAmount.Fixed(min)
|
|
||||||
else -> RuleAmount.Fixed(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RuleAmount.Fixed(s.toIntOrNull() ?: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v.isJsonObject -> {
|
|
||||||
val obj = v.asJsonObject
|
|
||||||
val min = obj.get("min")?.takeIf { it.isJsonPrimitive }?.asInt
|
|
||||||
val max = obj.get("max")?.takeIf { it.isJsonPrimitive }?.asInt
|
|
||||||
val value = obj.get("value")?.takeIf { it.isJsonPrimitive }?.asInt
|
|
||||||
when {
|
|
||||||
min != null && max != null -> RuleAmount.Range(min, max)
|
|
||||||
value != null -> RuleAmount.Fixed(value)
|
|
||||||
else -> RuleAmount.Fixed(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> RuleAmount.Fixed(0)
|
|
||||||
}
|
|
||||||
key to amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val addObj = root.getAsJsonObject("add")
|
|
||||||
val subObj = root.getAsJsonObject("sub")
|
|
||||||
PointsRules(
|
|
||||||
add = parseBlock(addObj),
|
|
||||||
sub = parseBlock(subObj)
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局可观察的积分余额(仅内存,不落盘)
|
|
||||||
private val _pointsBalance = MutableStateFlow<PointsBalance?>(null)
|
|
||||||
val pointsBalance: StateFlow<PointsBalance?> = _pointsBalance.asStateFlow()
|
|
||||||
|
|
||||||
// 当前已加载的用户ID,用于处理用户切换
|
|
||||||
@Volatile private var currentUserId: Int? = null
|
|
||||||
|
|
||||||
/** 设置当前用户ID;当用户切换时,会清空旧的积分数据以避免串号 */
|
|
||||||
fun setCurrentUser(userId: Int?) {
|
|
||||||
if (currentUserId != userId) {
|
|
||||||
currentUserId = userId
|
|
||||||
_pointsBalance.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清空内存中的积分状态(用于登出或用户切换) */
|
|
||||||
fun clear() {
|
|
||||||
_pointsBalance.value = null
|
|
||||||
currentUserId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新当前用户的积分余额(进入应用并完成登录态初始化后调用)
|
|
||||||
* - 若为游客或无 token,则清空并返回
|
|
||||||
*/
|
|
||||||
suspend fun refreshMyPointsBalance(includeStatistics: Boolean = true) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (AppStore.isGuest || AppStore.token == null) {
|
|
||||||
clear()
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
val balance = getMyPointsBalance(includeStatistics)
|
|
||||||
_pointsBalance.value = balance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前用户积分余额
|
|
||||||
*
|
|
||||||
* @param includeStatistics 是否包含统计信息(累计获得和累计消费),默认 true
|
|
||||||
* @return 积分余额信息,包含当前余额和可选的统计数据
|
|
||||||
* @throws Exception 网络请求失败或服务器返回错误
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* try {
|
|
||||||
* // 获取包含统计信息的积分余额
|
|
||||||
* val balance = PointService.getMyPointsBalance()
|
|
||||||
* println("当前余额: ${balance.balance}")
|
|
||||||
* println("累计获得: ${balance.totalEarned}")
|
|
||||||
* println("累计消费: ${balance.totalSpent}")
|
|
||||||
*
|
|
||||||
* // 仅获取当前余额
|
|
||||||
* val simpleBalance = PointService.getMyPointsBalance(includeStatistics = false)
|
|
||||||
* println("当前余额: ${simpleBalance.balance}")
|
|
||||||
* } catch (e: Exception) {
|
|
||||||
* println("获取积分余额失败: ${e.message}")
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
suspend fun getMyPointsBalance(includeStatistics: Boolean = true): PointsBalance {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response = ApiClient.api.getMyPointsBalance(includeStatistics)
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body()?.data ?: throw Exception("响应数据为空")
|
|
||||||
} else {
|
|
||||||
throw Exception("获取积分余额失败: ${response.code()} ${response.message()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前用户积分变更日志列表
|
|
||||||
*
|
|
||||||
* @param page 页码,默认 1
|
|
||||||
* @param pageSize 每页数量,默认 20
|
|
||||||
* @param changeType 变更类型筛选("add": 增加, "subtract": 减少, "adjust": 调整),null 表示不筛选
|
|
||||||
* @param startTime 开始时间,格式:YYYY-MM-DD,null 表示不限制
|
|
||||||
* @param endTime 结束时间,格式:YYYY-MM-DD,null 表示不限制
|
|
||||||
* @return 积分变更日志列表响应,包含日志列表和分页信息
|
|
||||||
* @throws Exception 网络请求失败或服务器返回错误
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* try {
|
|
||||||
* // 获取最近的积分变更日志
|
|
||||||
* val logs = PointService.getMyPointsChangeLogs(page = 1, pageSize = 20)
|
|
||||||
* println("总记录数: ${logs.total}")
|
|
||||||
* logs.list.forEach { log ->
|
|
||||||
* println("${log.createdAt}: ${log.changeType} ${log.amount} (${log.reason})")
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 筛选积分增加记录
|
|
||||||
* val earnLogs = PointService.getMyPointsChangeLogs(changeType = "add")
|
|
||||||
*
|
|
||||||
* // 查询指定时间范围的记录
|
|
||||||
* val rangeLogs = PointService.getMyPointsChangeLogs(
|
|
||||||
* startTime = "2024-01-01",
|
|
||||||
* endTime = "2024-01-31"
|
|
||||||
* )
|
|
||||||
* } catch (e: Exception) {
|
|
||||||
* println("获取积分变更日志失败: ${e.message}")
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
suspend fun getMyPointsChangeLogs(
|
|
||||||
page: Int = 1,
|
|
||||||
pageSize: Int = 20,
|
|
||||||
changeType: String? = null,
|
|
||||||
startTime: String? = null,
|
|
||||||
endTime: String? = null
|
|
||||||
): PointsChangeLogsResponse {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response =
|
|
||||||
ApiClient.api.getMyPointsChangeLogs(
|
|
||||||
page = page,
|
|
||||||
pageSize = pageSize,
|
|
||||||
changeType = changeType,
|
|
||||||
startTime = startTime,
|
|
||||||
endTime = endTime
|
|
||||||
)
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body() ?: throw Exception("响应数据为空")
|
|
||||||
} else {
|
|
||||||
throw Exception("获取积分变更日志失败: ${response.code()} ${response.message()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 积分变更类型常量 */
|
|
||||||
object ChangeType {
|
|
||||||
/** 积分增加 */
|
|
||||||
const val ADD = "add"
|
|
||||||
/** 积分减少 */
|
|
||||||
const val SUBTRACT = "subtract"
|
|
||||||
/** 积分调整 */
|
|
||||||
const val ADJUST = "adjust"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 积分变更原因常量 */
|
|
||||||
object ChangeReason {
|
|
||||||
// 获得积分类型
|
|
||||||
/** 新用户注册奖励 */
|
|
||||||
const val EARN_REGISTER = "earn_register"
|
|
||||||
/** 每日签到奖励 */
|
|
||||||
const val EARN_DAILY = "earn_daily"
|
|
||||||
/** 任务完成奖励 */
|
|
||||||
const val EARN_TASK = "earn_task"
|
|
||||||
/** 邀请好友奖励 */
|
|
||||||
const val EARN_INVITE = "earn_invite"
|
|
||||||
/** 充值获得 */
|
|
||||||
const val EARN_RECHARGE = "earn_recharge"
|
|
||||||
|
|
||||||
// 消费积分类型
|
|
||||||
/** 创建群聊 */
|
|
||||||
const val SPEND_GROUP_CREATE = "spend_group_create"
|
|
||||||
/** 扩容群聊 */
|
|
||||||
const val SPEND_GROUP_EXPAND = "spend_group_expand"
|
|
||||||
/** Agent 私密模式 */
|
|
||||||
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
|
|
||||||
/** Agent 记忆添加 */
|
|
||||||
const val SPEND_AGENT_MEMORY = "spend_agent_memory"
|
|
||||||
/** 房间记忆添加 */
|
|
||||||
const val SPEND_ROOM_MEMORY = "spend_room_memory"
|
|
||||||
/** 自定义聊天背景 */
|
|
||||||
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
|
|
||||||
/** 定时事件解锁 */
|
|
||||||
const val SPEND_SCHEDULE_EVENT = "spend_schedule_event"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取变更原因的描述(支持多语言)
|
|
||||||
*
|
|
||||||
* @param context Context 用于获取资源
|
|
||||||
* @param reason 变更原因代码
|
|
||||||
* @return 本地化描述
|
|
||||||
*/
|
|
||||||
fun getReasonDescription(context: Context, reason: String): String {
|
|
||||||
val resourceId = when (reason) {
|
|
||||||
ChangeReason.EARN_REGISTER -> R.string.earn_register
|
|
||||||
ChangeReason.EARN_DAILY -> R.string.earn_daily
|
|
||||||
ChangeReason.EARN_TASK -> R.string.earn_task
|
|
||||||
ChangeReason.EARN_INVITE -> R.string.earn_invite
|
|
||||||
ChangeReason.EARN_RECHARGE -> R.string.earn_recharge
|
|
||||||
ChangeReason.SPEND_GROUP_CREATE -> R.string.spend_group_create
|
|
||||||
ChangeReason.SPEND_GROUP_EXPAND -> R.string.spend_group_expand
|
|
||||||
ChangeReason.SPEND_AGENT_PRIVATE -> R.string.spend_agent_private
|
|
||||||
ChangeReason.SPEND_AGENT_MEMORY -> R.string.spend_agent_memory
|
|
||||||
ChangeReason.SPEND_ROOM_MEMORY -> R.string.spend_room_memory
|
|
||||||
ChangeReason.SPEND_CHAT_BACKGROUND -> R.string.spend_chat_background
|
|
||||||
ChangeReason.SPEND_SCHEDULE_EVENT -> R.string.spend_schedule_event
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
return resourceId?.let { context.getString(it) } ?: reason // 未知原因,返回原始代码
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取变更类型的中文描述
|
|
||||||
*
|
|
||||||
* @param changeType 变更类型代码
|
|
||||||
* @return 中文描述
|
|
||||||
*/
|
|
||||||
fun getChangeTypeDescription(changeType: String): String {
|
|
||||||
return when (changeType) {
|
|
||||||
ChangeType.ADD -> "增加"
|
|
||||||
ChangeType.SUBTRACT -> "减少"
|
|
||||||
ChangeType.ADJUST -> "调整"
|
|
||||||
else -> changeType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
package com.aiosman.ravenow.data
|
|
||||||
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonSyntaxException
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推荐接口响应
|
|
||||||
*/
|
|
||||||
data class RecommendationsResponse(
|
|
||||||
@SerializedName("success")
|
|
||||||
val success: Boolean,
|
|
||||||
@SerializedName("data")
|
|
||||||
val data: List<RecommendationItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推荐项
|
|
||||||
*/
|
|
||||||
data class RecommendationItem(
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Long,
|
|
||||||
@SerializedName("data")
|
|
||||||
val data: Any // 根据type字段动态解析为不同类型
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* 将data字段转换为PromptRecommendationData
|
|
||||||
*/
|
|
||||||
fun toPromptData(): PromptRecommendationData? {
|
|
||||||
if (type != "prompt") return null
|
|
||||||
return try {
|
|
||||||
val gson = Gson()
|
|
||||||
val jsonString = gson.toJson(data)
|
|
||||||
gson.fromJson(jsonString, PromptRecommendationData::class.java)
|
|
||||||
} catch (e: JsonSyntaxException) {
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将data字段转换为PostRecommendationData (Moment)
|
|
||||||
*/
|
|
||||||
fun toPostData(): PostRecommendationData? {
|
|
||||||
if (type !in listOf("post_normal", "post_video", "post_news", "post_music")) return null
|
|
||||||
return try {
|
|
||||||
val gson = Gson()
|
|
||||||
val jsonString = gson.toJson(data)
|
|
||||||
gson.fromJson(jsonString, PostRecommendationData::class.java)
|
|
||||||
} catch (e: JsonSyntaxException) {
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将data字段转换为RoomRecommendationData
|
|
||||||
*/
|
|
||||||
fun toRoomData(): RoomRecommendationData? {
|
|
||||||
if (type != "room") return null
|
|
||||||
return try {
|
|
||||||
val gson = Gson()
|
|
||||||
val jsonString = gson.toJson(data)
|
|
||||||
gson.fromJson(jsonString, RoomRecommendationData::class.java)
|
|
||||||
} catch (e: JsonSyntaxException) {
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt类型推荐数据
|
|
||||||
*/
|
|
||||||
data class PromptRecommendationData(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Long,
|
|
||||||
@SerializedName("title")
|
|
||||||
val title: String,
|
|
||||||
@SerializedName("desc")
|
|
||||||
val desc: String,
|
|
||||||
@SerializedName("createdAt")
|
|
||||||
val createdAt: String,
|
|
||||||
@SerializedName("updatedAt")
|
|
||||||
val updatedAt: String,
|
|
||||||
@SerializedName("avatar")
|
|
||||||
val avatar: String? = null,
|
|
||||||
@SerializedName("avatarDirectUrl")
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
@SerializedName("author")
|
|
||||||
val author: String? = null,
|
|
||||||
@SerializedName("isPublic")
|
|
||||||
val isPublic: Boolean = false,
|
|
||||||
@SerializedName("openId")
|
|
||||||
val openId: String,
|
|
||||||
@SerializedName("breakMode")
|
|
||||||
val breakMode: Boolean = false,
|
|
||||||
@SerializedName("useCount")
|
|
||||||
val useCount: Int? = null,
|
|
||||||
@SerializedName("translations")
|
|
||||||
val translations: Map<String, Map<String, String>>? = null,
|
|
||||||
@SerializedName("translation")
|
|
||||||
val translation: Map<String, String>? = null,
|
|
||||||
@SerializedName("details")
|
|
||||||
val details: PromptDetails? = null,
|
|
||||||
@SerializedName("aiUserProfile")
|
|
||||||
val aiUserProfile: AIUserProfile? = null,
|
|
||||||
@SerializedName("creatorProfile")
|
|
||||||
val creatorProfile: CreatorProfile? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt详细信息
|
|
||||||
*/
|
|
||||||
data class PromptDetails(
|
|
||||||
@SerializedName("gender")
|
|
||||||
val gender: String? = null,
|
|
||||||
@SerializedName("age")
|
|
||||||
val age: Int? = null,
|
|
||||||
@SerializedName("mbti")
|
|
||||||
val mbti: String? = null,
|
|
||||||
@SerializedName("constellation")
|
|
||||||
val constellation: String? = null,
|
|
||||||
@SerializedName("nickname")
|
|
||||||
val nickname: String? = null,
|
|
||||||
@SerializedName("birthday")
|
|
||||||
val birthday: String? = null,
|
|
||||||
@SerializedName("signature")
|
|
||||||
val signature: String? = null,
|
|
||||||
@SerializedName("nationality")
|
|
||||||
val nationality: String? = null,
|
|
||||||
@SerializedName("mainLanguage")
|
|
||||||
val mainLanguage: String? = null,
|
|
||||||
@SerializedName("worldview")
|
|
||||||
val worldview: String? = null,
|
|
||||||
@SerializedName("habits")
|
|
||||||
val habits: String? = null,
|
|
||||||
@SerializedName("hobbies")
|
|
||||||
val hobbies: String? = null,
|
|
||||||
@SerializedName("occupation")
|
|
||||||
val occupation: String? = null,
|
|
||||||
@SerializedName("expertise")
|
|
||||||
val expertise: String? = null,
|
|
||||||
@SerializedName("socialActivityLvl")
|
|
||||||
val socialActivityLvl: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI用户资料
|
|
||||||
*/
|
|
||||||
data class AIUserProfile(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Long,
|
|
||||||
@SerializedName("username")
|
|
||||||
val username: String? = null,
|
|
||||||
@SerializedName("nickname")
|
|
||||||
val nickname: String,
|
|
||||||
@SerializedName("avatar")
|
|
||||||
val avatar: String? = null,
|
|
||||||
@SerializedName("avatarMedium")
|
|
||||||
val avatarMedium: String? = null,
|
|
||||||
@SerializedName("avatarLarge")
|
|
||||||
val avatarLarge: String? = null,
|
|
||||||
@SerializedName("avatarDirectUrl")
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarMediumDirectUrl")
|
|
||||||
val avatarMediumDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarLargeDirectUrl")
|
|
||||||
val avatarLargeDirectUrl: String? = null,
|
|
||||||
@SerializedName("bio")
|
|
||||||
val bio: String? = null,
|
|
||||||
@SerializedName("trtcUserId")
|
|
||||||
val trtcUserId: String? = null,
|
|
||||||
@SerializedName("chatAIId")
|
|
||||||
val chatAIId: String? = null,
|
|
||||||
@SerializedName("aiAccount")
|
|
||||||
val aiAccount: Boolean = true,
|
|
||||||
@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 CreatorProfile(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Long,
|
|
||||||
@SerializedName("username")
|
|
||||||
val username: String? = null,
|
|
||||||
@SerializedName("nickname")
|
|
||||||
val nickname: String,
|
|
||||||
@SerializedName("avatar")
|
|
||||||
val avatar: String? = null,
|
|
||||||
@SerializedName("avatarMedium")
|
|
||||||
val avatarMedium: String? = null,
|
|
||||||
@SerializedName("avatarLarge")
|
|
||||||
val avatarLarge: String? = null,
|
|
||||||
@SerializedName("avatarDirectUrl")
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarMediumDirectUrl")
|
|
||||||
val avatarMediumDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarLargeDirectUrl")
|
|
||||||
val avatarLargeDirectUrl: String? = null,
|
|
||||||
@SerializedName("bio")
|
|
||||||
val bio: String? = null,
|
|
||||||
@SerializedName("trtcUserId")
|
|
||||||
val trtcUserId: String? = null,
|
|
||||||
@SerializedName("chatAIId")
|
|
||||||
val chatAIId: 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
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Post类型推荐数据(复用Moment中的结构)
|
|
||||||
* 支持 post_normal, post_video, post_news, post_music
|
|
||||||
*/
|
|
||||||
typealias PostRecommendationData = Moment
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Room类型推荐数据
|
|
||||||
*/
|
|
||||||
data class RoomRecommendationData(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Long,
|
|
||||||
@SerializedName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerializedName("description")
|
|
||||||
val description: String,
|
|
||||||
@SerializedName("createdAt")
|
|
||||||
val createdAt: String? = null,
|
|
||||||
@SerializedName("updatedAt")
|
|
||||||
val updatedAt: String? = null,
|
|
||||||
@SerializedName("cover")
|
|
||||||
val cover: String? = null,
|
|
||||||
@SerializedName("coverDirectUrl")
|
|
||||||
val coverDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatar")
|
|
||||||
val avatar: String? = null,
|
|
||||||
@SerializedName("avatarDirectUrl")
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
@SerializedName("recommendBanner")
|
|
||||||
val recommendBanner: String? = null,
|
|
||||||
@SerializedName("trtcRoomId")
|
|
||||||
val trtcRoomId: String? = null,
|
|
||||||
@SerializedName("trtcType")
|
|
||||||
val trtcType: String? = null,
|
|
||||||
@SerializedName("isRecommended")
|
|
||||||
val isRecommended: Boolean = false,
|
|
||||||
@SerializedName("allowInHot")
|
|
||||||
val allowInHot: Boolean = false,
|
|
||||||
@SerializedName("language")
|
|
||||||
val language: String? = null,
|
|
||||||
@SerializedName("maxTotal")
|
|
||||||
val maxTotal: Int? = null,
|
|
||||||
@SerializedName("privateFeePaid")
|
|
||||||
val privateFeePaid: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推荐Service接口
|
|
||||||
*/
|
|
||||||
interface RecommendationService {
|
|
||||||
/**
|
|
||||||
* 获取推荐列表
|
|
||||||
* @param pool 推荐池名称(可选)
|
|
||||||
* @param count 返回数量(可选,默认10,最大50)
|
|
||||||
* @param lang 语言代码(可选)
|
|
||||||
* @param promptReplaceTrans 是否用翻译覆盖原字段(可选,仅对prompt类型生效)
|
|
||||||
* @return 推荐项列表
|
|
||||||
*/
|
|
||||||
suspend fun getRecommendations(
|
|
||||||
pool: String? = null,
|
|
||||||
count: Int? = null,
|
|
||||||
lang: String? = null,
|
|
||||||
promptReplaceTrans: Boolean? = null
|
|
||||||
): List<RecommendationItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推荐Service实现
|
|
||||||
*/
|
|
||||||
class RecommendationServiceImpl : RecommendationService {
|
|
||||||
override suspend fun getRecommendations(
|
|
||||||
pool: String?,
|
|
||||||
count: Int?,
|
|
||||||
lang: String?,
|
|
||||||
promptReplaceTrans: Boolean?
|
|
||||||
): List<RecommendationItem> {
|
|
||||||
val resp = ApiClient.api.getRecommendations(
|
|
||||||
pool = pool,
|
|
||||||
count = count,
|
|
||||||
lang = lang,
|
|
||||||
promptReplaceTrans = promptReplaceTrans
|
|
||||||
)
|
|
||||||
if (resp.isSuccessful) {
|
|
||||||
val body = resp.body()
|
|
||||||
if (body != null && body.success) {
|
|
||||||
return body.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw ServiceException("Failed to get recommendations")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CreatorProfile 扩展函数,转换为 CreatorProfileEntity
|
|
||||||
*/
|
|
||||||
fun CreatorProfile.toCreatorProfileEntity(): com.aiosman.ravenow.entity.CreatorProfileEntity {
|
|
||||||
return com.aiosman.ravenow.entity.CreatorProfileEntity(
|
|
||||||
id = id,
|
|
||||||
username = username,
|
|
||||||
nickname = nickname,
|
|
||||||
avatar = avatar?.let {
|
|
||||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
|
||||||
},
|
|
||||||
bio = bio,
|
|
||||||
trtcUserId = trtcUserId,
|
|
||||||
chatAIId = chatAIId,
|
|
||||||
aiAccount = aiAccount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -6,19 +6,7 @@ import com.aiosman.ravenow.data.api.UpdateRoomRuleRequestBody
|
|||||||
import com.aiosman.ravenow.data.api.RoomRuleQuota
|
import com.aiosman.ravenow.data.api.RoomRuleQuota
|
||||||
import com.aiosman.ravenow.data.api.RoomRule
|
import com.aiosman.ravenow.data.api.RoomRule
|
||||||
import com.aiosman.ravenow.data.api.RoomRuleCreator
|
import com.aiosman.ravenow.data.api.RoomRuleCreator
|
||||||
import com.aiosman.ravenow.entity.AddAgentToRoomFailedItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.AddAgentToRoomItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.AddAgentToRoomResultEntity
|
|
||||||
import com.aiosman.ravenow.entity.AddUserToRoomFailedItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.AddUserToRoomItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.AddUserToRoomResultEntity
|
|
||||||
import com.aiosman.ravenow.entity.CreatorEntity
|
import com.aiosman.ravenow.entity.CreatorEntity
|
||||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomFailedItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.RemoveAgentFromRoomResultEntity
|
|
||||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomFailedItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomItemEntity
|
|
||||||
import com.aiosman.ravenow.entity.RemoveUserFromRoomResultEntity
|
|
||||||
import com.aiosman.ravenow.entity.RoomEntity
|
import com.aiosman.ravenow.entity.RoomEntity
|
||||||
import com.aiosman.ravenow.entity.RoomRuleEntity
|
import com.aiosman.ravenow.entity.RoomRuleEntity
|
||||||
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
|
import com.aiosman.ravenow.entity.RoomRuleCreatorEntity
|
||||||
@@ -26,22 +14,6 @@ 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
|
||||||
|
|
||||||
/**
|
|
||||||
* 房间内的智能体信息(PromptTemplate)
|
|
||||||
*/
|
|
||||||
data class PromptTemplate(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerializedName("openId")
|
|
||||||
val openId: String,
|
|
||||||
@SerializedName("title")
|
|
||||||
val title: String,
|
|
||||||
@SerializedName("desc")
|
|
||||||
val desc: String,
|
|
||||||
@SerializedName("avatar")
|
|
||||||
val avatar: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Room(
|
data class Room(
|
||||||
@SerializedName("id")
|
@SerializedName("id")
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -67,26 +39,12 @@ data class Room(
|
|||||||
val creator: Creator,
|
val creator: Creator,
|
||||||
@SerializedName("userCount")
|
@SerializedName("userCount")
|
||||||
val userCount: Int,
|
val userCount: Int,
|
||||||
@SerializedName("totalMemberCount")
|
|
||||||
val totalMemberCount: Int? = null,
|
|
||||||
@SerializedName("maxMemberLimit")
|
@SerializedName("maxMemberLimit")
|
||||||
val maxMemberLimit: Int,
|
val maxMemberLimit: Int,
|
||||||
@SerializedName("maxTotal")
|
|
||||||
val maxTotal: Int? = null,
|
|
||||||
@SerializedName("systemMaxTotal")
|
|
||||||
val systemMaxTotal: Int? = null,
|
|
||||||
@SerializedName("canJoin")
|
@SerializedName("canJoin")
|
||||||
val canJoin: Boolean,
|
val canJoin: Boolean,
|
||||||
@SerializedName("canJoinCode")
|
@SerializedName("canJoinCode")
|
||||||
val canJoinCode: Int,
|
val canJoinCode: Int,
|
||||||
@SerializedName("privateFeePaid")
|
|
||||||
val privateFeePaid: Boolean? = null,
|
|
||||||
@SerializedName("prompts")
|
|
||||||
val prompts: List<PromptTemplate>? = null,
|
|
||||||
@SerializedName("createdAt")
|
|
||||||
val createdAt: String? = null,
|
|
||||||
@SerializedName("updatedAt")
|
|
||||||
val updatedAt: String? = null,
|
|
||||||
@SerializedName("users")
|
@SerializedName("users")
|
||||||
val users: List<Users>
|
val users: List<Users>
|
||||||
|
|
||||||
@@ -105,24 +63,9 @@ data class Room(
|
|||||||
allowInHot = allowInHot,
|
allowInHot = allowInHot,
|
||||||
creator = creator.toCreatorEntity(),
|
creator = creator.toCreatorEntity(),
|
||||||
userCount = userCount,
|
userCount = userCount,
|
||||||
totalMemberCount = totalMemberCount,
|
|
||||||
maxMemberLimit = maxMemberLimit,
|
maxMemberLimit = maxMemberLimit,
|
||||||
maxTotal = maxTotal,
|
|
||||||
systemMaxTotal = systemMaxTotal,
|
|
||||||
canJoin = canJoin,
|
canJoin = canJoin,
|
||||||
canJoinCode = canJoinCode,
|
canJoinCode = canJoinCode,
|
||||||
privateFeePaid = privateFeePaid ?: false,
|
|
||||||
prompts = prompts?.map {
|
|
||||||
com.aiosman.ravenow.entity.PromptTemplateEntity(
|
|
||||||
id = it.id,
|
|
||||||
openId = it.openId,
|
|
||||||
title = it.title,
|
|
||||||
desc = it.desc,
|
|
||||||
avatar = it.avatar
|
|
||||||
)
|
|
||||||
} ?: emptyList(),
|
|
||||||
createdAt = createdAt,
|
|
||||||
updatedAt = updatedAt,
|
|
||||||
users = users.map { it.toUsersEntity() }
|
users = users.map { it.toUsersEntity() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -135,7 +78,7 @@ data class Creator(
|
|||||||
@SerializedName("userId")
|
@SerializedName("userId")
|
||||||
val userId: String,
|
val userId: String,
|
||||||
@SerializedName("trtcUserId")
|
@SerializedName("trtcUserId")
|
||||||
val trtcUserId: String? = null,
|
val trtcUserId: String,
|
||||||
@SerializedName("profile")
|
@SerializedName("profile")
|
||||||
val profile: Profile
|
val profile: Profile
|
||||||
){
|
){
|
||||||
@@ -143,7 +86,7 @@ data class Creator(
|
|||||||
return CreatorEntity(
|
return CreatorEntity(
|
||||||
id = id,
|
id = id,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
trtcUserId = trtcUserId ?: "",
|
trtcUserId = trtcUserId,
|
||||||
profile = profile.toProfileEntity()
|
profile = profile.toProfileEntity()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,7 +98,7 @@ data class Users(
|
|||||||
@SerializedName("userId")
|
@SerializedName("userId")
|
||||||
val userId: String,
|
val userId: String,
|
||||||
@SerializedName("trtcUserId")
|
@SerializedName("trtcUserId")
|
||||||
val trtcUserId: String? = null,
|
val trtcUserId: String,
|
||||||
@SerializedName("profile")
|
@SerializedName("profile")
|
||||||
val profile: Profile
|
val profile: Profile
|
||||||
){
|
){
|
||||||
@@ -230,68 +173,6 @@ interface RoomService {
|
|||||||
roomId: Int? = null,
|
roomId: Int? = null,
|
||||||
trtcId: String? = null
|
trtcId: String? = null
|
||||||
): RoomRuleQuotaEntity
|
): RoomRuleQuotaEntity
|
||||||
|
|
||||||
// ========== Room Member Management ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID,与 trtcId 二选一
|
|
||||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
|
||||||
* @param openIds 要添加的用户OpenID列表
|
|
||||||
* @return 添加结果实体
|
|
||||||
* @throws ServiceException 添加失败时抛出异常
|
|
||||||
*/
|
|
||||||
suspend fun addUserToRoom(
|
|
||||||
roomId: Int? = null,
|
|
||||||
trtcId: String? = null,
|
|
||||||
openIds: List<String>
|
|
||||||
): AddUserToRoomResultEntity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID,与 trtcId 二选一
|
|
||||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
|
||||||
* @param agentOpenIds 要添加的智能体OpenID列表
|
|
||||||
* @return 添加结果实体
|
|
||||||
* @throws ServiceException 添加失败时抛出异常
|
|
||||||
*/
|
|
||||||
suspend fun addAgentToRoom(
|
|
||||||
roomId: Int? = null,
|
|
||||||
trtcId: String? = null,
|
|
||||||
agentOpenIds: List<String>
|
|
||||||
): AddAgentToRoomResultEntity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除智能体
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID,与 trtcId 二选一
|
|
||||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
|
||||||
* @param agentOpenIds 要移除的智能体OpenID列表
|
|
||||||
* @return 移除结果实体
|
|
||||||
* @throws ServiceException 移除失败时抛出异常
|
|
||||||
*/
|
|
||||||
suspend fun removeAgentFromRoom(
|
|
||||||
roomId: Int? = null,
|
|
||||||
trtcId: String? = null,
|
|
||||||
agentOpenIds: List<String>
|
|
||||||
): RemoveAgentFromRoomResultEntity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除用户
|
|
||||||
*
|
|
||||||
* @param roomId 房间ID,与 trtcId 二选一
|
|
||||||
* @param trtcId TRTC群组ID,与 roomId 二选一
|
|
||||||
* @param userIds 要移除的用户ID列表(OpenID)
|
|
||||||
* @return 移除结果实体
|
|
||||||
* @throws ServiceException 移除失败时抛出异常
|
|
||||||
*/
|
|
||||||
suspend fun removeUserFromRoom(
|
|
||||||
roomId: Int? = null,
|
|
||||||
trtcId: String? = null,
|
|
||||||
userIds: List<String>
|
|
||||||
): RemoveUserFromRoomResultEntity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -372,78 +253,6 @@ class RoomServiceImpl : RoomService {
|
|||||||
|
|
||||||
return data.toRoomRuleQuotaEntity()
|
return data.toRoomRuleQuotaEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addUserToRoom(
|
|
||||||
roomId: Int?,
|
|
||||||
trtcId: String?,
|
|
||||||
openIds: List<String>
|
|
||||||
): AddUserToRoomResultEntity {
|
|
||||||
val resp = ApiClient.api.addUserToRoom(
|
|
||||||
com.aiosman.ravenow.data.api.AddUserToRoomRequestBody(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
openIds = openIds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: throw ServiceException("添加用户到房间失败")
|
|
||||||
val data = body.data ?: throw ServiceException("添加用户响应数据为空")
|
|
||||||
|
|
||||||
return data.result.toAddUserToRoomResultEntity()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun addAgentToRoom(
|
|
||||||
roomId: Int?,
|
|
||||||
trtcId: String?,
|
|
||||||
agentOpenIds: List<String>
|
|
||||||
): AddAgentToRoomResultEntity {
|
|
||||||
val resp = ApiClient.api.addAgentToRoom(
|
|
||||||
com.aiosman.ravenow.data.api.AddAgentToRoomRequestBody(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
agentOpenIds = agentOpenIds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: throw ServiceException("添加智能体到房间失败")
|
|
||||||
val data = body.data ?: throw ServiceException("添加智能体响应数据为空")
|
|
||||||
|
|
||||||
return data.result.toAddAgentToRoomResultEntity()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun removeAgentFromRoom(
|
|
||||||
roomId: Int?,
|
|
||||||
trtcId: String?,
|
|
||||||
agentOpenIds: List<String>
|
|
||||||
): RemoveAgentFromRoomResultEntity {
|
|
||||||
val resp = ApiClient.api.removeAgentFromRoom(
|
|
||||||
com.aiosman.ravenow.data.api.RemoveAgentFromRoomRequestBody(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
agentOpenIds = agentOpenIds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: throw ServiceException("从房间移除智能体失败")
|
|
||||||
val data = body.data ?: throw ServiceException("移除智能体响应数据为空")
|
|
||||||
|
|
||||||
return data.toRemoveAgentFromRoomResultEntity()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun removeUserFromRoom(
|
|
||||||
roomId: Int?,
|
|
||||||
trtcId: String?,
|
|
||||||
userIds: List<String>
|
|
||||||
): RemoveUserFromRoomResultEntity {
|
|
||||||
val resp = ApiClient.api.removeUserFromRoom(
|
|
||||||
com.aiosman.ravenow.data.api.RemoveUserFromRoomRequestBody(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
userIds = userIds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: throw ServiceException("从房间移除用户失败")
|
|
||||||
val data = body.data ?: throw ServiceException("移除用户响应数据为空")
|
|
||||||
|
|
||||||
return data.toRemoveUserFromRoomResultEntity()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -468,12 +277,7 @@ fun RoomRuleCreator.toRoomRuleCreatorEntity(): RoomRuleCreatorEntity {
|
|||||||
return RoomRuleCreatorEntity(
|
return RoomRuleCreatorEntity(
|
||||||
id = id,
|
id = id,
|
||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
avatar = avatar,
|
avatar = avatar
|
||||||
avatarMedium = avatarMedium,
|
|
||||||
avatarLarge = avatarLarge,
|
|
||||||
avatarDirectUrl = avatarDirectUrl,
|
|
||||||
avatarMediumDirectUrl = avatarMediumDirectUrl,
|
|
||||||
avatarLargeDirectUrl = avatarLargeDirectUrl
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,128 +295,6 @@ fun RoomRuleQuota.toRoomRuleQuotaEntity(): RoomRuleQuotaEntity {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Room Member Management 扩展函数 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AddUserToRoomResult 扩展函数,转换为 AddUserToRoomResultEntity
|
|
||||||
*/
|
|
||||||
fun com.aiosman.ravenow.data.api.AddUserToRoomResult.toAddUserToRoomResultEntity(): AddUserToRoomResultEntity {
|
|
||||||
return AddUserToRoomResultEntity(
|
|
||||||
totalCount = totalCount,
|
|
||||||
successCount = successCount,
|
|
||||||
failedCount = failedCount,
|
|
||||||
skippedCount = skippedCount,
|
|
||||||
successItems = successItems.map { it.toAddUserToRoomItemEntity() },
|
|
||||||
failedItems = failedItems.map { it.toAddUserToRoomFailedItemEntity() },
|
|
||||||
skippedItems = skippedItems.map { it.toAddUserToRoomItemEntity() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.AddUserToRoomItem.toAddUserToRoomItemEntity(): AddUserToRoomItemEntity {
|
|
||||||
return AddUserToRoomItemEntity(
|
|
||||||
userId = userId,
|
|
||||||
type = type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.AddUserToRoomFailedItem.toAddUserToRoomFailedItemEntity(): AddUserToRoomFailedItemEntity {
|
|
||||||
return AddUserToRoomFailedItemEntity(
|
|
||||||
userId = userId,
|
|
||||||
type = type,
|
|
||||||
error = error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AddAgentToRoomResult 扩展函数,转换为 AddAgentToRoomResultEntity
|
|
||||||
*/
|
|
||||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomResult.toAddAgentToRoomResultEntity(): AddAgentToRoomResultEntity {
|
|
||||||
return AddAgentToRoomResultEntity(
|
|
||||||
totalCount = totalCount,
|
|
||||||
successCount = successCount,
|
|
||||||
failedCount = failedCount,
|
|
||||||
skippedCount = skippedCount,
|
|
||||||
successItems = successItems.map { it.toAddAgentToRoomItemEntity() },
|
|
||||||
failedItems = failedItems.map { it.toAddAgentToRoomFailedItemEntity() },
|
|
||||||
skippedItems = skippedItems.map { it.toAddAgentToRoomItemEntity() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomItem.toAddAgentToRoomItemEntity(): AddAgentToRoomItemEntity {
|
|
||||||
return AddAgentToRoomItemEntity(
|
|
||||||
agentOpenId = agentOpenId,
|
|
||||||
type = type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.AddAgentToRoomFailedItem.toAddAgentToRoomFailedItemEntity(): AddAgentToRoomFailedItemEntity {
|
|
||||||
return AddAgentToRoomFailedItemEntity(
|
|
||||||
agentOpenId = agentOpenId,
|
|
||||||
type = type,
|
|
||||||
error = error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RemoveAgentFromRoomResult 扩展函数,转换为 RemoveAgentFromRoomResultEntity
|
|
||||||
*/
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomResult.toRemoveAgentFromRoomResultEntity(): RemoveAgentFromRoomResultEntity {
|
|
||||||
return RemoveAgentFromRoomResultEntity(
|
|
||||||
totalCount = totalCount,
|
|
||||||
successCount = successCount,
|
|
||||||
failedCount = failedCount,
|
|
||||||
skippedCount = skippedCount,
|
|
||||||
successItems = successItems.map { it.toRemoveAgentFromRoomItemEntity() },
|
|
||||||
failedItems = failedItems.map { it.toRemoveAgentFromRoomFailedItemEntity() },
|
|
||||||
skippedItems = skippedItems.map { it.toRemoveAgentFromRoomItemEntity() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomItem.toRemoveAgentFromRoomItemEntity(): RemoveAgentFromRoomItemEntity {
|
|
||||||
return RemoveAgentFromRoomItemEntity(
|
|
||||||
agentOpenId = agentOpenId,
|
|
||||||
type = type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveAgentFromRoomFailedItem.toRemoveAgentFromRoomFailedItemEntity(): RemoveAgentFromRoomFailedItemEntity {
|
|
||||||
return RemoveAgentFromRoomFailedItemEntity(
|
|
||||||
agentOpenId = agentOpenId,
|
|
||||||
type = type,
|
|
||||||
error = error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RemoveUserFromRoomResult 扩展函数,转换为 RemoveUserFromRoomResultEntity
|
|
||||||
*/
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomResult.toRemoveUserFromRoomResultEntity(): RemoveUserFromRoomResultEntity {
|
|
||||||
return RemoveUserFromRoomResultEntity(
|
|
||||||
totalCount = totalCount,
|
|
||||||
successCount = successCount,
|
|
||||||
failedCount = failedCount,
|
|
||||||
skippedCount = skippedCount,
|
|
||||||
successItems = successItems.map { it.toRemoveUserFromRoomItemEntity() },
|
|
||||||
failedItems = failedItems.map { it.toRemoveUserFromRoomFailedItemEntity() },
|
|
||||||
skippedItems = skippedItems.map { it.toRemoveUserFromRoomItemEntity() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomItem.toRemoveUserFromRoomItemEntity(): RemoveUserFromRoomItemEntity {
|
|
||||||
return RemoveUserFromRoomItemEntity(
|
|
||||||
userId = userId,
|
|
||||||
type = type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun com.aiosman.ravenow.data.api.RemoveUserFromRoomFailedItem.toRemoveUserFromRoomFailedItemEntity(): RemoveUserFromRoomFailedItemEntity {
|
|
||||||
return RemoveUserFromRoomFailedItemEntity(
|
|
||||||
userId = userId,
|
|
||||||
type = type,
|
|
||||||
error = error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.aiosman.ravenow.data
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.BatchTrtcUserIdRequestBody
|
|
||||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||||
|
|
||||||
data class UserAuth(
|
data class UserAuth(
|
||||||
@@ -47,8 +46,7 @@ interface UserService {
|
|||||||
page: Int = 1,
|
page: Int = 1,
|
||||||
nickname: String? = null,
|
nickname: String? = null,
|
||||||
followerId: Int? = null,
|
followerId: Int? = null,
|
||||||
followingId: Int? = null,
|
followingId: Int? = null
|
||||||
roomId: Int? = null
|
|
||||||
): ListContainer<AccountProfileEntity>
|
): ListContainer<AccountProfileEntity>
|
||||||
|
|
||||||
|
|
||||||
@@ -68,16 +66,6 @@ interface UserService {
|
|||||||
|
|
||||||
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
|
suspend fun getUserProfileByOpenId(id: String):AccountProfileEntity
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量通过 TRTC 用户ID 获取用户信息列表
|
|
||||||
* @param ids TRTC 用户ID列表(最多100个)
|
|
||||||
* @param includeAI 是否包含AI账号,默认 false
|
|
||||||
* @return 用户信息实体列表
|
|
||||||
*/
|
|
||||||
suspend fun getUserProfilesByTrtcUserIds(
|
|
||||||
ids: List<String>,
|
|
||||||
includeAI: Boolean = false
|
|
||||||
): List<AccountProfileEntity>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserServiceImpl : UserService {
|
class UserServiceImpl : UserService {
|
||||||
@@ -102,8 +90,7 @@ class UserServiceImpl : UserService {
|
|||||||
page: Int,
|
page: Int,
|
||||||
nickname: String?,
|
nickname: String?,
|
||||||
followerId: Int?,
|
followerId: Int?,
|
||||||
followingId: Int?,
|
followingId: Int?
|
||||||
roomId: Int?
|
|
||||||
): ListContainer<AccountProfileEntity> {
|
): ListContainer<AccountProfileEntity> {
|
||||||
val resp = ApiClient.api.getUsers(
|
val resp = ApiClient.api.getUsers(
|
||||||
page = page,
|
page = page,
|
||||||
@@ -111,7 +98,7 @@ class UserServiceImpl : UserService {
|
|||||||
search = nickname,
|
search = nickname,
|
||||||
followerId = followerId,
|
followerId = followerId,
|
||||||
followingId = followingId,
|
followingId = followingId,
|
||||||
includeAI = true,
|
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>(
|
||||||
@@ -133,18 +120,4 @@ class UserServiceImpl : UserService {
|
|||||||
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
val body = resp.body() ?: throw ServiceException("Failed to get account")
|
||||||
return body.data.toAccountProfileEntity()
|
return body.data.toAccountProfileEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserProfilesByTrtcUserIds(
|
|
||||||
ids: List<String>,
|
|
||||||
includeAI: Boolean
|
|
||||||
): List<AccountProfileEntity> {
|
|
||||||
val resp = ApiClient.api.getAccountProfilesByTrtcBatch(
|
|
||||||
BatchTrtcUserIdRequestBody(
|
|
||||||
trtcUserIds = ids,
|
|
||||||
includeAI = includeAI
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: throw ServiceException("Failed to get accounts")
|
|
||||||
return body.data.map { it.toAccountProfileEntity() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,6 @@ import com.aiosman.ravenow.data.Comment
|
|||||||
import com.aiosman.ravenow.data.DataContainer
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
import com.aiosman.ravenow.data.Moment
|
import com.aiosman.ravenow.data.Moment
|
||||||
import com.aiosman.ravenow.data.RecommendationsResponse
|
|
||||||
import com.aiosman.ravenow.data.Room
|
import com.aiosman.ravenow.data.Room
|
||||||
import com.aiosman.ravenow.entity.ChatNotification
|
import com.aiosman.ravenow.entity.ChatNotification
|
||||||
import com.aiosman.ravenow.data.membership.MembershipConfigData
|
import com.aiosman.ravenow.data.membership.MembershipConfigData
|
||||||
@@ -87,13 +86,6 @@ data class JoinGroupChatRequestBody(
|
|||||||
val roomId: Int? = null,
|
val roomId: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BatchTrtcUserIdRequestBody(
|
|
||||||
@SerializedName("trtcUserIds")
|
|
||||||
val trtcUserIds: List<String>,
|
|
||||||
@SerializedName("includeAI")
|
|
||||||
val includeAI: Boolean? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class LoginUserRequestBody(
|
data class LoginUserRequestBody(
|
||||||
@SerializedName("username")
|
@SerializedName("username")
|
||||||
val username: String? = null,
|
val username: String? = null,
|
||||||
@@ -281,260 +273,6 @@ data class RemoveAccountRequestBody(
|
|||||||
val password: String,
|
val password: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== Room Member Management 相关数据类 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间请求体
|
|
||||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
|
||||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
|
||||||
* @param openIds 要添加的用户OpenID列表
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomRequestBody(
|
|
||||||
@SerializedName("roomId")
|
|
||||||
val roomId: Int? = null,
|
|
||||||
@SerializedName("trtcId")
|
|
||||||
val trtcId: String? = null,
|
|
||||||
@SerializedName("openIds")
|
|
||||||
val openIds: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间请求体
|
|
||||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
|
||||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
|
||||||
* @param agentOpenIds 要添加的智能体OpenID列表
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomRequestBody(
|
|
||||||
@SerializedName("roomId")
|
|
||||||
val roomId: Int? = null,
|
|
||||||
@SerializedName("trtcId")
|
|
||||||
val trtcId: String? = null,
|
|
||||||
@SerializedName("agentOpenIds")
|
|
||||||
val agentOpenIds: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除智能体请求体
|
|
||||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
|
||||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
|
||||||
* @param agentOpenIds 要移除的智能体OpenID列表
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomRequestBody(
|
|
||||||
@SerializedName("roomId")
|
|
||||||
val roomId: Int? = null,
|
|
||||||
@SerializedName("trtcId")
|
|
||||||
val trtcId: String? = null,
|
|
||||||
@SerializedName("agentOpenIds")
|
|
||||||
val agentOpenIds: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除用户请求体
|
|
||||||
* @param roomId 房间ID(与trtcId互斥,二者必须提供其一)
|
|
||||||
* @param trtcId TRTC群组ID(与roomId互斥,二者必须提供其一)
|
|
||||||
* @param userIds 要移除的用户ID列表(OpenID)
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomRequestBody(
|
|
||||||
@SerializedName("roomId")
|
|
||||||
val roomId: Int? = null,
|
|
||||||
@SerializedName("trtcId")
|
|
||||||
val trtcId: String? = null,
|
|
||||||
@SerializedName("userIds")
|
|
||||||
val userIds: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户成功项目
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomItem(
|
|
||||||
@SerializedName("userId")
|
|
||||||
val userId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户失败项目
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomFailedItem(
|
|
||||||
@SerializedName("userId")
|
|
||||||
val userId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerializedName("error")
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间的结果
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomResult(
|
|
||||||
@SerializedName("totalCount")
|
|
||||||
val totalCount: Int,
|
|
||||||
@SerializedName("successCount")
|
|
||||||
val successCount: Int,
|
|
||||||
@SerializedName("failedCount")
|
|
||||||
val failedCount: Int,
|
|
||||||
@SerializedName("skippedCount")
|
|
||||||
val skippedCount: Int,
|
|
||||||
@SerializedName("successItems")
|
|
||||||
val successItems: List<AddUserToRoomItem>,
|
|
||||||
@SerializedName("failedItems")
|
|
||||||
val failedItems: List<AddUserToRoomFailedItem>,
|
|
||||||
@SerializedName("skippedItems")
|
|
||||||
val skippedItems: List<AddUserToRoomItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间响应
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomResponse(
|
|
||||||
@SerializedName("message")
|
|
||||||
val message: String,
|
|
||||||
@SerializedName("operationType")
|
|
||||||
val operationType: String,
|
|
||||||
@SerializedName("result")
|
|
||||||
val result: AddUserToRoomResult
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体成功项目
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomItem(
|
|
||||||
@SerializedName("agentOpenId")
|
|
||||||
val agentOpenId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体失败项目
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomFailedItem(
|
|
||||||
@SerializedName("agentOpenId")
|
|
||||||
val agentOpenId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerializedName("error")
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间的结果
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomResult(
|
|
||||||
@SerializedName("totalCount")
|
|
||||||
val totalCount: Int,
|
|
||||||
@SerializedName("successCount")
|
|
||||||
val successCount: Int,
|
|
||||||
@SerializedName("failedCount")
|
|
||||||
val failedCount: Int,
|
|
||||||
@SerializedName("skippedCount")
|
|
||||||
val skippedCount: Int,
|
|
||||||
@SerializedName("successItems")
|
|
||||||
val successItems: List<AddAgentToRoomItem>,
|
|
||||||
@SerializedName("failedItems")
|
|
||||||
val failedItems: List<AddAgentToRoomFailedItem>,
|
|
||||||
@SerializedName("skippedItems")
|
|
||||||
val skippedItems: List<AddAgentToRoomItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间响应
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomResponse(
|
|
||||||
@SerializedName("message")
|
|
||||||
val message: String,
|
|
||||||
@SerializedName("operationType")
|
|
||||||
val operationType: String,
|
|
||||||
@SerializedName("result")
|
|
||||||
val result: AddAgentToRoomResult
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除智能体成功项目
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomItem(
|
|
||||||
@SerializedName("agentOpenId")
|
|
||||||
val agentOpenId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除智能体失败项目
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomFailedItem(
|
|
||||||
@SerializedName("agentOpenId")
|
|
||||||
val agentOpenId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerializedName("error")
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除智能体的结果
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomResult(
|
|
||||||
@SerializedName("totalCount")
|
|
||||||
val totalCount: Int,
|
|
||||||
@SerializedName("successCount")
|
|
||||||
val successCount: Int,
|
|
||||||
@SerializedName("failedCount")
|
|
||||||
val failedCount: Int,
|
|
||||||
@SerializedName("skippedCount")
|
|
||||||
val skippedCount: Int,
|
|
||||||
@SerializedName("successItems")
|
|
||||||
val successItems: List<RemoveAgentFromRoomItem>,
|
|
||||||
@SerializedName("failedItems")
|
|
||||||
val failedItems: List<RemoveAgentFromRoomFailedItem>,
|
|
||||||
@SerializedName("skippedItems")
|
|
||||||
val skippedItems: List<RemoveAgentFromRoomItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除用户成功项目
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomItem(
|
|
||||||
@SerializedName("userId")
|
|
||||||
val userId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除用户失败项目
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomFailedItem(
|
|
||||||
@SerializedName("userId")
|
|
||||||
val userId: String,
|
|
||||||
@SerializedName("type")
|
|
||||||
val type: String,
|
|
||||||
@SerializedName("error")
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除用户的结果
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomResult(
|
|
||||||
@SerializedName("totalCount")
|
|
||||||
val totalCount: Int,
|
|
||||||
@SerializedName("successCount")
|
|
||||||
val successCount: Int,
|
|
||||||
@SerializedName("failedCount")
|
|
||||||
val failedCount: Int,
|
|
||||||
@SerializedName("skippedCount")
|
|
||||||
val skippedCount: Int,
|
|
||||||
@SerializedName("successItems")
|
|
||||||
val successItems: List<RemoveUserFromRoomItem>,
|
|
||||||
@SerializedName("failedItems")
|
|
||||||
val failedItems: List<RemoveUserFromRoomFailedItem>,
|
|
||||||
@SerializedName("skippedItems")
|
|
||||||
val skippedItems: List<RemoveUserFromRoomItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
// API 错误响应(用于加入房间等接口的错误处理)
|
// API 错误响应(用于加入房间等接口的错误处理)
|
||||||
data class ApiErrorResponse(
|
data class ApiErrorResponse(
|
||||||
@SerializedName("err")
|
@SerializedName("err")
|
||||||
@@ -913,11 +651,6 @@ data class UpdateRoomRuleRequestBody(
|
|||||||
* @param id 创建者ID
|
* @param id 创建者ID
|
||||||
* @param nickname 创建者昵称
|
* @param nickname 创建者昵称
|
||||||
* @param avatar 创建者头像文件名
|
* @param avatar 创建者头像文件名
|
||||||
* @param avatarMedium 中等头像文件名
|
|
||||||
* @param avatarLarge 大头像文件名
|
|
||||||
* @param avatarDirectUrl 小头像直接访问URL
|
|
||||||
* @param avatarMediumDirectUrl 中等头像直接访问URL
|
|
||||||
* @param avatarLargeDirectUrl 大头像直接访问URL
|
|
||||||
*/
|
*/
|
||||||
data class RoomRuleCreator(
|
data class RoomRuleCreator(
|
||||||
@SerializedName("id")
|
@SerializedName("id")
|
||||||
@@ -925,17 +658,7 @@ data class RoomRuleCreator(
|
|||||||
@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("avatarDirectUrl")
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarMediumDirectUrl")
|
|
||||||
val avatarMediumDirectUrl: String? = null,
|
|
||||||
@SerializedName("avatarLargeDirectUrl")
|
|
||||||
val avatarLargeDirectUrl: String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1038,71 +761,6 @@ data class InsufficientBalanceError(
|
|||||||
val traceId: String?
|
val traceId: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== Points 相关数据类 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分余额信息
|
|
||||||
* @param balance 当前积分余额
|
|
||||||
* @param totalEarned 累计获得积分(可选)
|
|
||||||
* @param totalSpent 累计消费积分(可选)
|
|
||||||
*/
|
|
||||||
data class PointsBalance(
|
|
||||||
@SerializedName("balance")
|
|
||||||
val balance: Int,
|
|
||||||
@SerializedName("totalEarned")
|
|
||||||
val totalEarned: Int? = null,
|
|
||||||
@SerializedName("totalSpent")
|
|
||||||
val totalSpent: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分变更日志
|
|
||||||
* @param id 日志记录ID
|
|
||||||
* @param changeType 变更类型(add: 增加, subtract: 减少, adjust: 调整)
|
|
||||||
* @param before 变更前余额
|
|
||||||
* @param after 变更后余额
|
|
||||||
* @param amount 本次变更数量(增加为正数,减少为负数)
|
|
||||||
* @param reason 变更原因代码
|
|
||||||
* @param createdAt 创建时间,格式:YYYY-MM-DD HH:mm:ss
|
|
||||||
*/
|
|
||||||
data class PointsChangeLog(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerializedName("changeType")
|
|
||||||
val changeType: String,
|
|
||||||
@SerializedName("before")
|
|
||||||
val before: Int,
|
|
||||||
@SerializedName("after")
|
|
||||||
val after: Int,
|
|
||||||
@SerializedName("amount")
|
|
||||||
val amount: Int,
|
|
||||||
@SerializedName("reason")
|
|
||||||
val reason: String,
|
|
||||||
@SerializedName("createdAt")
|
|
||||||
val createdAt: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 积分变更日志列表响应
|
|
||||||
* @param success 请求是否成功
|
|
||||||
* @param list 积分变更日志列表
|
|
||||||
* @param total 总记录数
|
|
||||||
* @param page 当前页码
|
|
||||||
* @param pageSize 每页数量
|
|
||||||
*/
|
|
||||||
data class PointsChangeLogsResponse(
|
|
||||||
@SerializedName("success")
|
|
||||||
val success: Boolean,
|
|
||||||
@SerializedName("list")
|
|
||||||
val list: List<PointsChangeLog>,
|
|
||||||
@SerializedName("total")
|
|
||||||
val total: Int,
|
|
||||||
@SerializedName("page")
|
|
||||||
val page: Int,
|
|
||||||
@SerializedName("pageSize")
|
|
||||||
val pageSize: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
interface RaveNowAPI {
|
interface RaveNowAPI {
|
||||||
@GET("membership/config")
|
@GET("membership/config")
|
||||||
@retrofit2.http.Headers("X-Requires-Auth: true")
|
@retrofit2.http.Headers("X-Requires-Auth: true")
|
||||||
@@ -1142,7 +800,6 @@ interface RaveNowAPI {
|
|||||||
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
||||||
@Query("explore") explore: String? = null,
|
@Query("explore") explore: String? = null,
|
||||||
@Query("newsFilter") newsFilter: String? = null,
|
@Query("newsFilter") newsFilter: String? = null,
|
||||||
@Query("videoFilter") videoFilter: String? = null,
|
|
||||||
): Response<ListContainer<Moment>>
|
): Response<ListContainer<Moment>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -1280,11 +937,6 @@ interface RaveNowAPI {
|
|||||||
@Path("id") id: String
|
@Path("id") id: String
|
||||||
): Response<DataContainer<AccountProfile>>
|
): Response<DataContainer<AccountProfile>>
|
||||||
|
|
||||||
@POST("profile/trtc/batch")
|
|
||||||
suspend fun getAccountProfilesByTrtcBatch(
|
|
||||||
@Body body: BatchTrtcUserIdRequestBody
|
|
||||||
): Response<DataContainer<List<AccountProfile>>>
|
|
||||||
|
|
||||||
@POST("user/{id}/follow")
|
@POST("user/{id}/follow")
|
||||||
suspend fun followUser(
|
suspend fun followUser(
|
||||||
@Path("id") id: Int
|
@Path("id") id: Int
|
||||||
@@ -1350,16 +1002,6 @@ interface RaveNowAPI {
|
|||||||
@Query("keys") keys: String
|
@Query("keys") keys: String
|
||||||
): Response<ListContainer<DictItem>>
|
): Response<ListContainer<DictItem>>
|
||||||
|
|
||||||
@GET("/outside/dict")
|
|
||||||
suspend fun getOutsideDict(
|
|
||||||
@Query("key") key: String
|
|
||||||
): Response<DataContainer<DictItem>>
|
|
||||||
|
|
||||||
@GET("/outside/dicts")
|
|
||||||
suspend fun getOutsideDicts(
|
|
||||||
@Query("keys") keys: String
|
|
||||||
): Response<ListContainer<DictItem>>
|
|
||||||
|
|
||||||
@POST("captcha/generate")
|
@POST("captcha/generate")
|
||||||
suspend fun generateCaptcha(
|
suspend fun generateCaptcha(
|
||||||
@Body body: CaptchaRequestBody
|
@Body body: CaptchaRequestBody
|
||||||
@@ -1403,9 +1045,6 @@ interface RaveNowAPI {
|
|||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
@Query("categoryIds") categoryIds: List<Int>? = null,
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
@Query("random") random: Int? = null,
|
@Query("random") random: Int? = null,
|
||||||
@Query("title") title: String? = null,
|
|
||||||
@Query("desc") desc: String? = null,
|
|
||||||
@Query("excludeRoomId") excludeRoomId: Int? = null,
|
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
@@ -1448,39 +1087,11 @@ interface RaveNowAPI {
|
|||||||
@POST("outside/rooms")
|
@POST("outside/rooms")
|
||||||
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>
|
suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response<DataContainer<Unit>>
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取房间列表
|
|
||||||
*
|
|
||||||
* 支持游客和认证用户访问,根据用户类型返回不同的房间数据。
|
|
||||||
* 游客模式返回公开推荐房间列表,认证用户模式返回用户可访问的群聊列表。
|
|
||||||
*
|
|
||||||
* @param page 页码,默认 1
|
|
||||||
* @param pageSize 每页数量,默认 20(游客模式最大50)
|
|
||||||
* @param roomId 房间ID,用于精确查询特定房间(仅认证用户)
|
|
||||||
* @param includeUsers 是否包含用户列表,默认false(仅认证用户)
|
|
||||||
* @param isRecommended 是否推荐过滤器:1=推荐,0=非推荐,null=不过滤(仅认证用户)
|
|
||||||
* @param roomType 房间类型过滤:all=公有私有都显示, public=只显示公有, private=只显示私有, created=只显示自己创建的, joined=只显示自己加入的(仅认证用户)
|
|
||||||
* @param search 搜索关键字,支持房间名称、描述、智能体名称模糊匹配
|
|
||||||
* @param random 是否随机排序,字符串长度不为0则为true(传任意非空字符串即可)
|
|
||||||
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
|
||||||
* @param showPublic 是否显示公有房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户)
|
|
||||||
* @param showCreated 是否显示自己创建的房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户)
|
|
||||||
* @param showJoined 是否显示自己加入的房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户)
|
|
||||||
*/
|
|
||||||
@GET("outside/rooms")
|
@GET("outside/rooms")
|
||||||
suspend fun getRooms(
|
suspend fun getRooms(@Query("page") page: Int = 1,
|
||||||
@Query("page") page: Int = 1,
|
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("roomId") roomId: Long? = null,
|
@Query("isRecommended") isRecommended: Int = 1,
|
||||||
@Query("includeUsers") includeUsers: Boolean? = null,
|
@Query("random") random: Int? = null,
|
||||||
@Query("isRecommended") isRecommended: Int? = null,
|
|
||||||
@Query("roomType") roomType: String? = null,
|
|
||||||
@Query("search") search: String? = null,
|
|
||||||
@Query("random") random: String? = null,
|
|
||||||
@Query("ownerSessionId") ownerSessionId: String? = null,
|
|
||||||
@Query("showPublic") showPublic: Boolean? = null,
|
|
||||||
@Query("showCreated") showCreated: Boolean? = null,
|
|
||||||
@Query("showJoined") showJoined: Boolean? = null,
|
|
||||||
): Response<ListContainer<Room>>
|
): Response<ListContainer<Room>>
|
||||||
|
|
||||||
@GET("outside/rooms/detail")
|
@GET("outside/rooms/detail")
|
||||||
@@ -1525,35 +1136,6 @@ interface RaveNowAPI {
|
|||||||
@Query("pageSize") pageSize: Int? = null
|
@Query("pageSize") pageSize: Int? = null
|
||||||
): Response<ListContainer<Agent>>
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Prompt详情(支持ID或OpenId)
|
|
||||||
* @param promptId Prompt ID或OpenId(UUID格式)
|
|
||||||
*/
|
|
||||||
@GET("outside/prompt/{promptId}")
|
|
||||||
suspend fun getPromptDetail(
|
|
||||||
@Path("promptId") promptId: String
|
|
||||||
): Response<DataContainer<Agent>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新Prompt(支持ID或OpenId)
|
|
||||||
* @param promptId Prompt ID或OpenId(UUID格式)
|
|
||||||
* @param avatar 头像文件(可选)
|
|
||||||
* @param title 标题(可选)
|
|
||||||
* @param desc 描述(可选)
|
|
||||||
* @param value 内容(可选)
|
|
||||||
* @param isPublic 是否公开(可选)
|
|
||||||
*/
|
|
||||||
@Multipart
|
|
||||||
@PATCH("outside/prompt/{promptId}")
|
|
||||||
suspend fun updatePrompt(
|
|
||||||
@Path("promptId") promptId: String,
|
|
||||||
@Part avatar: MultipartBody.Part?,
|
|
||||||
@Part("title") title: RequestBody?,
|
|
||||||
@Part("desc") desc: RequestBody?,
|
|
||||||
@Part("value") value: RequestBody?,
|
|
||||||
@Part("public") isPublic: RequestBody?,
|
|
||||||
): Response<DataContainer<Agent>>
|
|
||||||
|
|
||||||
// ========== Agent Rule API ==========
|
// ========== Agent Rule API ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2010,217 +1592,5 @@ interface RaveNowAPI {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GET("recommendations")
|
|
||||||
suspend fun getRecommendations(
|
|
||||||
@Query("pool") pool: String? = null,
|
|
||||||
@Query("count") count: Int? = null,
|
|
||||||
@Query("lang") lang: String? = null,
|
|
||||||
@Query("promptReplaceTrans") promptReplaceTrans: Boolean? = null
|
|
||||||
): Response<RecommendationsResponse>
|
|
||||||
|
|
||||||
// ========== Points API ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取我的积分余额
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 获取当前登录用户的积分余额
|
|
||||||
* - 可选返回累计获得和累计消费统计信息
|
|
||||||
*
|
|
||||||
* @param includeStatistics 是否包含统计信息(累计获得和累计消费),默认 true
|
|
||||||
*
|
|
||||||
* @return 返回积分余额和统计信息
|
|
||||||
*
|
|
||||||
* 响应数据说明:
|
|
||||||
* - balance: 当前积分余额
|
|
||||||
* - totalEarned: 累计获得积分(仅当 includeStatistics 为 true 时返回)
|
|
||||||
* - totalSpent: 累计消费积分(仅当 includeStatistics 为 true 时返回)
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* // 获取包含统计信息的积分余额
|
|
||||||
* val response1 = api.getMyPointsBalance()
|
|
||||||
*
|
|
||||||
* // 仅获取当前余额
|
|
||||||
* val response2 = api.getMyPointsBalance(includeStatistics = false)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@GET("account/my/points")
|
|
||||||
suspend fun getMyPointsBalance(
|
|
||||||
@Query("includeStatistics") includeStatistics: Boolean? = null
|
|
||||||
): Response<DataContainer<PointsBalance>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取我的积分变更日志
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 获取当前登录用户的积分变更日志列表
|
|
||||||
* - 支持分页、时间范围筛选和变更类型筛选
|
|
||||||
*
|
|
||||||
* @param page 页码,默认 1
|
|
||||||
* @param pageSize 每页数量,默认 20
|
|
||||||
* @param changeType 变更类型筛选(add: 增加, subtract: 减少, adjust: 调整)
|
|
||||||
* @param startTime 开始时间,格式:YYYY-MM-DD
|
|
||||||
* @param endTime 结束时间,格式:YYYY-MM-DD
|
|
||||||
*
|
|
||||||
* @return 返回分页的积分变更日志列表
|
|
||||||
*
|
|
||||||
* 响应数据说明:
|
|
||||||
* - list: 积分变更日志列表
|
|
||||||
* - total: 总记录数
|
|
||||||
* - page: 当前页码
|
|
||||||
* - pageSize: 每页数量
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* // 获取最近的积分变更日志
|
|
||||||
* val response1 = api.getMyPointsChangeLogs(page = 1, pageSize = 20)
|
|
||||||
*
|
|
||||||
* // 筛选积分增加记录
|
|
||||||
* val response2 = api.getMyPointsChangeLogs(changeType = "add")
|
|
||||||
*
|
|
||||||
* // 查询指定时间范围的记录
|
|
||||||
* val response3 = api.getMyPointsChangeLogs(
|
|
||||||
* startTime = "2024-01-01",
|
|
||||||
* endTime = "2024-01-31"
|
|
||||||
* )
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@GET("account/my/points/logs")
|
|
||||||
suspend fun getMyPointsChangeLogs(
|
|
||||||
@Query("page") page: Int = 1,
|
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
|
||||||
@Query("changeType") changeType: String? = null,
|
|
||||||
@Query("startTime") startTime: String? = null,
|
|
||||||
@Query("endTime") endTime: String? = null
|
|
||||||
): Response<PointsChangeLogsResponse>
|
|
||||||
|
|
||||||
// ========== Room Member Management API ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 向房间批量添加用户
|
|
||||||
* - 支持通过房间ID或TRTC群组ID添加
|
|
||||||
* - 只有房间创建者可以使用此接口
|
|
||||||
* - 添加数量受房间容量限制
|
|
||||||
* - 成功添加后会自动扣除房间创建者的积分用于扩容
|
|
||||||
*
|
|
||||||
* @param body 添加用户请求体
|
|
||||||
* - roomId: 房间ID(与 trtcId 二选一)
|
|
||||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
|
||||||
* - openIds: 要添加的用户OpenID列表
|
|
||||||
*
|
|
||||||
* @return 成功时返回操作结果,包含成功、失败、跳过的用户列表
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* val request = AddUserToRoomRequestBody(
|
|
||||||
* roomId = 123,
|
|
||||||
* openIds = listOf("user_openid_1", "user_openid_2")
|
|
||||||
* )
|
|
||||||
* val response = api.addUserToRoom(request)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@POST("outside/rooms/add-user")
|
|
||||||
suspend fun addUserToRoom(
|
|
||||||
@Body body: AddUserToRoomRequestBody
|
|
||||||
): Response<DataContainer<AddUserToRoomResponse>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 向房间批量添加智能体(Agent)
|
|
||||||
* - 支持通过房间ID或TRTC群组ID添加
|
|
||||||
* - 房间创建者可以使用此接口
|
|
||||||
* - 当容量不足时会自动扩容并扣除房间创建者的积分
|
|
||||||
* - 如果积分不足以支付扩容费用,将返回错误
|
|
||||||
*
|
|
||||||
* @param body 添加智能体请求体
|
|
||||||
* - roomId: 房间ID(与 trtcId 二选一)
|
|
||||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
|
||||||
* - agentOpenIds: 要添加的智能体OpenID列表
|
|
||||||
*
|
|
||||||
* @return 成功时返回操作结果,包含成功、失败、跳过的智能体列表
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* val request = AddAgentToRoomRequestBody(
|
|
||||||
* roomId = 123,
|
|
||||||
* agentOpenIds = listOf("agent_openid_1", "agent_openid_2")
|
|
||||||
* )
|
|
||||||
* val response = api.addAgentToRoom(request)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@POST("outside/rooms/add-agent")
|
|
||||||
suspend fun addAgentToRoom(
|
|
||||||
@Body body: AddAgentToRoomRequestBody
|
|
||||||
): Response<DataContainer<AddAgentToRoomResponse>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除智能体
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 从房间批量移除智能体(Agent)
|
|
||||||
* - 支持通过房间ID或TRTC群组ID操作
|
|
||||||
* - 只有房间创建者可以使用此接口
|
|
||||||
* - 单聊房间不支持移除智能体
|
|
||||||
* - 移除操作会同步到OpenIM系统
|
|
||||||
*
|
|
||||||
* @param body 移除智能体请求体
|
|
||||||
* - roomId: 房间ID(与 trtcId 二选一)
|
|
||||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
|
||||||
* - agentOpenIds: 要移除的智能体OpenID列表
|
|
||||||
*
|
|
||||||
* @return 成功时返回操作结果,包含成功、失败、跳过的智能体列表
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* val request = RemoveAgentFromRoomRequestBody(
|
|
||||||
* roomId = 123,
|
|
||||||
* agentOpenIds = listOf("agent_openid_1", "agent_openid_2")
|
|
||||||
* )
|
|
||||||
* val response = api.removeAgentFromRoom(request)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@POST("outside/rooms/remove-agent")
|
|
||||||
suspend fun removeAgentFromRoom(
|
|
||||||
@Body body: RemoveAgentFromRoomRequestBody
|
|
||||||
): Response<DataContainer<RemoveAgentFromRoomResult>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除用户
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* - 从房间批量移除用户
|
|
||||||
* - 支持通过房间ID或TRTC群组ID操作
|
|
||||||
* - 只有房间创建者可以使用此接口
|
|
||||||
* - 单聊房间不支持移除用户
|
|
||||||
* - 群主不能移除自己
|
|
||||||
* - 移除操作会同步到OpenIM系统
|
|
||||||
*
|
|
||||||
* @param body 移除用户请求体
|
|
||||||
* - roomId: 房间ID(与 trtcId 二选一)
|
|
||||||
* - trtcId: TRTC群组ID(与 roomId 二选一)
|
|
||||||
* - userIds: 要移除的用户ID列表(OpenID)
|
|
||||||
*
|
|
||||||
* @return 成功时返回操作结果,包含成功、失败、跳过的用户列表
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* ```kotlin
|
|
||||||
* val request = RemoveUserFromRoomRequestBody(
|
|
||||||
* roomId = 123,
|
|
||||||
* userIds = listOf("user_openid_1", "user_openid_2")
|
|
||||||
* )
|
|
||||||
* val response = api.removeUserFromRoom(request)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
@POST("outside/rooms/remove-user")
|
|
||||||
suspend fun removeUserFromRoom(
|
|
||||||
@Body body: RemoveUserFromRoomRequestBody
|
|
||||||
): Response<DataContainer<RemoveUserFromRoomResult>>
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.aiosman.ravenow.data.db
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import androidx.room.Query
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.Upsert
|
|
||||||
|
|
||||||
@Entity(tableName = "trtc_participant_cache")
|
|
||||||
data class TrtcParticipantCache(
|
|
||||||
@PrimaryKey val trtcId: String,
|
|
||||||
val isAI: Boolean,
|
|
||||||
val updatedAt: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface TrtcParticipantCacheDao {
|
|
||||||
@Query("SELECT * FROM trtc_participant_cache WHERE trtcId = :trtcId LIMIT 1")
|
|
||||||
suspend fun get(trtcId: String): TrtcParticipantCache?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM trtc_participant_cache WHERE trtcId IN (:ids)")
|
|
||||||
suspend fun getMany(ids: List<String>): List<TrtcParticipantCache>
|
|
||||||
|
|
||||||
@Upsert
|
|
||||||
suspend fun upsertAll(items: List<TrtcParticipantCache>)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
entities = [TrtcParticipantCache::class],
|
|
||||||
version = 1,
|
|
||||||
exportSchema = false
|
|
||||||
)
|
|
||||||
abstract class MessageCacheDatabase : RoomDatabase() {
|
|
||||||
abstract fun trtcParticipantCacheDao(): TrtcParticipantCacheDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Volatile
|
|
||||||
private var INSTANCE: MessageCacheDatabase? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context): MessageCacheDatabase {
|
|
||||||
return INSTANCE ?: synchronized(this) {
|
|
||||||
INSTANCE ?: Room.databaseBuilder(
|
|
||||||
context.applicationContext,
|
|
||||||
MessageCacheDatabase::class.java,
|
|
||||||
"message_cache.db"
|
|
||||||
).fallbackToDestructiveMigration()
|
|
||||||
.build()
|
|
||||||
.also { INSTANCE = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package com.aiosman.ravenow.data.repo
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.db.MessageCacheDatabase
|
|
||||||
import com.aiosman.ravenow.data.db.TrtcParticipantCache
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
object TrtcUserTypeRepository {
|
|
||||||
private const val TAG = "TrtcUserTypeRepo"
|
|
||||||
private const val MAX_BATCH = 100
|
|
||||||
private const val TTL_MS: Long = 5L * 24 * 60 * 60 * 1000 // 5 天
|
|
||||||
|
|
||||||
private val memoryCache = ConcurrentHashMap<String, CacheEntry>()
|
|
||||||
private var initialized = false
|
|
||||||
private lateinit var userService: UserService
|
|
||||||
private lateinit var db: com.aiosman.ravenow.data.db.MessageCacheDatabase
|
|
||||||
|
|
||||||
data class CacheEntry(
|
|
||||||
val isAI: Boolean,
|
|
||||||
val updatedAt: Long
|
|
||||||
) {
|
|
||||||
fun isExpired(now: Long): Boolean = now - updatedAt > TTL_MS
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureInit(context: Context) {
|
|
||||||
if (!initialized) {
|
|
||||||
db = MessageCacheDatabase.getInstance(context)
|
|
||||||
userService = UserServiceImpl()
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachedType(trtcId: String): Boolean? {
|
|
||||||
val entry = memoryCache[trtcId] ?: return null
|
|
||||||
return if (!entry.isExpired(System.currentTimeMillis())) entry.isAI else null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getType(context: Context, trtcId: String): Boolean? {
|
|
||||||
ensureInit(context)
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val mem = memoryCache[trtcId]
|
|
||||||
if (mem != null && !mem.isExpired(now)) return mem.isAI
|
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val dao = db.trtcParticipantCacheDao()
|
|
||||||
val entity = dao.get(trtcId)
|
|
||||||
if (entity != null && now - entity.updatedAt <= TTL_MS) {
|
|
||||||
memoryCache[trtcId] = CacheEntry(entity.isAI, entity.updatedAt)
|
|
||||||
entity.isAI
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ensureTypes(context: Context, trtcIds: List<String>) {
|
|
||||||
ensureInit(context)
|
|
||||||
if (trtcIds.isEmpty()) return
|
|
||||||
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
// 从内存/本地命中
|
|
||||||
val toFetch = withContext(Dispatchers.IO) {
|
|
||||||
val dao = db.trtcParticipantCacheDao()
|
|
||||||
val result = mutableSetOf<String>()
|
|
||||||
val needFromDb = mutableListOf<String>()
|
|
||||||
|
|
||||||
trtcIds.forEach { id ->
|
|
||||||
val mem = memoryCache[id]
|
|
||||||
if (mem == null || mem.isExpired(now)) {
|
|
||||||
needFromDb.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needFromDb.isNotEmpty()) {
|
|
||||||
val entities = dao.getMany(needFromDb)
|
|
||||||
val fromDbSet = entities.toMutableList()
|
|
||||||
// 写回内存并决定是否需要网络
|
|
||||||
val dbValid = mutableSetOf<String>()
|
|
||||||
fromDbSet.forEach { e ->
|
|
||||||
if (now - e.updatedAt <= TTL_MS) {
|
|
||||||
memoryCache[e.trtcId] = CacheEntry(e.isAI, e.updatedAt)
|
|
||||||
dbValid.add(e.trtcId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
needFromDb.forEach { id ->
|
|
||||||
if (!dbValid.contains(id)) {
|
|
||||||
result.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toFetch.isEmpty()) return
|
|
||||||
|
|
||||||
// 批量分片请求
|
|
||||||
val chunks = toFetch.chunked(MAX_BATCH)
|
|
||||||
for (chunk in chunks) {
|
|
||||||
try {
|
|
||||||
val profiles = withContext(Dispatchers.IO) {
|
|
||||||
userService.getUserProfilesByTrtcUserIds(chunk, includeAI = true)
|
|
||||||
}
|
|
||||||
// 将返回的 profile 按 trtcUserId -> isAI 映射
|
|
||||||
val upserts = profiles.mapNotNull { profile ->
|
|
||||||
val id = profile.trtcUserId
|
|
||||||
if (id.isNullOrEmpty()) null
|
|
||||||
else TrtcParticipantCache(
|
|
||||||
trtcId = id,
|
|
||||||
isAI = profile.aiAccount,
|
|
||||||
updatedAt = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 落库 + 内存
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
db.trtcParticipantCacheDao().upsertAll(upserts)
|
|
||||||
}
|
|
||||||
upserts.forEach { e ->
|
|
||||||
memoryCache[e.trtcId] = CacheEntry(e.isAI, e.updatedAt)
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Fetched types: size=${upserts.size}, batch=${chunk.size}")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "ensureTypes fetch failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -67,14 +67,6 @@ data class AccountProfileEntity(
|
|||||||
val rawAvatar: String,
|
val rawAvatar: String,
|
||||||
|
|
||||||
val chatAIId: String,
|
val chatAIId: String,
|
||||||
|
|
||||||
// AI角色背景图
|
|
||||||
val aiRoleAvatar: String? = null,
|
|
||||||
val aiRoleAvatarMedium: String? = null,
|
|
||||||
val aiRoleAvatarLarge: String? = null,
|
|
||||||
|
|
||||||
// 创建者信息(仅AI账号有)
|
|
||||||
val creatorProfile: CreatorProfileEntity? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,28 +110,6 @@ data class NoticeUserEntity(
|
|||||||
val avatar: String,
|
val avatar: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建者信息
|
|
||||||
*/
|
|
||||||
data class CreatorProfileEntity(
|
|
||||||
// 用户ID
|
|
||||||
val id: Long,
|
|
||||||
// 用户名
|
|
||||||
val username: String? = null,
|
|
||||||
// 昵称
|
|
||||||
val nickname: String,
|
|
||||||
// 头像
|
|
||||||
val avatar: String? = null,
|
|
||||||
// 个人简介
|
|
||||||
val bio: String? = null,
|
|
||||||
// trtcUserId
|
|
||||||
val trtcUserId: String? = null,
|
|
||||||
// chatAIId
|
|
||||||
val chatAIId: String? = null,
|
|
||||||
// 是否为AI账号
|
|
||||||
val aiAccount: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户点赞消息分页数据加载器
|
* 用户点赞消息分页数据加载器
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,35 +90,6 @@ class AgentPagingSource(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 智能体搜索分页加载器(按标题关键字)
|
|
||||||
*/
|
|
||||||
class AgentSearchPagingSource(
|
|
||||||
private val agentRemoteDataSource: AgentRemoteDataSource,
|
|
||||||
private val keyword: String,
|
|
||||||
) : PagingSource<Int, AgentEntity>() {
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AgentEntity> {
|
|
||||||
return try {
|
|
||||||
val currentPage = params.key ?: 1
|
|
||||||
val agents = agentRemoteDataSource.searchAgentByTitle(
|
|
||||||
pageNumber = currentPage,
|
|
||||||
title = keyword
|
|
||||||
)
|
|
||||||
LoadResult.Page(
|
|
||||||
data = agents?.list ?: listOf(),
|
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
|
||||||
nextKey = if (agents?.list?.isNotEmpty() == true) currentPage + 1 else null
|
|
||||||
)
|
|
||||||
} catch (exception: IOException) {
|
|
||||||
LoadResult.Error(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, AgentEntity>): Int? {
|
|
||||||
return state.anchorPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AgentRemoteDataSource(
|
class AgentRemoteDataSource(
|
||||||
private val agentService: AgentService,
|
private val agentService: AgentService,
|
||||||
@@ -132,16 +103,6 @@ class AgentRemoteDataSource(
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchAgentByTitle(
|
|
||||||
pageNumber: Int,
|
|
||||||
title: String
|
|
||||||
): ListContainer<AgentEntity>? {
|
|
||||||
return agentService.searchAgentByTitle(
|
|
||||||
pageNumber = pageNumber,
|
|
||||||
title = title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentServiceImpl() : AgentService {
|
class AgentServiceImpl() : AgentService {
|
||||||
@@ -157,17 +118,6 @@ class AgentServiceImpl() : AgentService {
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun searchAgentByTitle(
|
|
||||||
pageNumber: Int,
|
|
||||||
pageSize: Int,
|
|
||||||
title: String
|
|
||||||
): ListContainer<AgentEntity>? {
|
|
||||||
return agentBackend.searchAgentByTitle(
|
|
||||||
pageNumber = pageNumber,
|
|
||||||
title = title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentBackend {
|
class AgentBackend {
|
||||||
@@ -225,27 +175,6 @@ class AgentBackend {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchAgentByTitle(
|
|
||||||
pageNumber: Int,
|
|
||||||
title: String
|
|
||||||
): ListContainer<AgentEntity>? {
|
|
||||||
val resp = ApiClient.api.getAgent(
|
|
||||||
page = pageNumber,
|
|
||||||
pageSize = DataBatchSize,
|
|
||||||
withWorkflow = 1,
|
|
||||||
title = title
|
|
||||||
)
|
|
||||||
val body = resp.body() ?: return null
|
|
||||||
val dataContainer = body as DataContainer<ListContainer<Agent>>
|
|
||||||
val listContainer = dataContainer.data
|
|
||||||
return ListContainer(
|
|
||||||
total = listContainer.total,
|
|
||||||
page = pageNumber,
|
|
||||||
pageSize = DataBatchSize,
|
|
||||||
list = listContainer.list.map { it.toAgentEntity() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AgentEntity(
|
data class AgentEntity(
|
||||||
|
|||||||
@@ -13,6 +13,4 @@ data class GroupInfo(
|
|||||||
val groupAvatar: String,
|
val groupAvatar: String,
|
||||||
val memberCount: Int,
|
val memberCount: Int,
|
||||||
val isCreator: Boolean = false,
|
val isCreator: Boolean = false,
|
||||||
val trtcType: String = "Public",
|
|
||||||
val privateFeePaid: Boolean = false,
|
|
||||||
)
|
)
|
||||||
@@ -250,26 +250,8 @@ data class MomentImageEntity(
|
|||||||
val id: Long,
|
val id: Long,
|
||||||
// 图片URL
|
// 图片URL
|
||||||
val url: String,
|
val url: String,
|
||||||
// 原始图片URL
|
|
||||||
val originalUrl: String? = null,
|
|
||||||
// 直接访问URL
|
|
||||||
val directUrl: String? = null,
|
|
||||||
// 缩略图URL
|
// 缩略图URL
|
||||||
val thumbnail: String,
|
val thumbnail: String,
|
||||||
// 缩略图直接访问URL
|
|
||||||
val thumbnailDirectUrl: String? = null,
|
|
||||||
// 小尺寸图片URL
|
|
||||||
val small: String? = null,
|
|
||||||
// 小尺寸图片直接访问URL
|
|
||||||
val smallDirectUrl: String? = null,
|
|
||||||
// 中尺寸图片URL
|
|
||||||
val medium: String? = null,
|
|
||||||
// 中尺寸图片直接访问URL
|
|
||||||
val mediumDirectUrl: String? = null,
|
|
||||||
// 大尺寸图片URL
|
|
||||||
val large: String? = null,
|
|
||||||
// 大尺寸图片直接访问URL
|
|
||||||
val largeDirectUrl: String? = null,
|
|
||||||
// 图片BlurHash
|
// 图片BlurHash
|
||||||
val blurHash: String? = null,
|
val blurHash: String? = null,
|
||||||
// 宽度
|
// 宽度
|
||||||
@@ -278,38 +260,6 @@ 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
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态
|
* 动态
|
||||||
*/
|
*/
|
||||||
@@ -327,7 +277,7 @@ data class MomentEntity(
|
|||||||
// 是否关注
|
// 是否关注
|
||||||
val followStatus: Boolean,
|
val followStatus: Boolean,
|
||||||
// 动态内容
|
// 动态内容
|
||||||
val momentTextContent: String?,
|
val momentTextContent: String,
|
||||||
// 动态图片
|
// 动态图片
|
||||||
@DrawableRes val momentPicture: Int,
|
@DrawableRes val momentPicture: Int,
|
||||||
// 点赞数
|
// 点赞数
|
||||||
@@ -350,10 +300,6 @@ data class MomentEntity(
|
|||||||
var relMoment: MomentEntity? = null,
|
var relMoment: MomentEntity? = null,
|
||||||
// 是否收藏
|
// 是否收藏
|
||||||
var isFavorite: Boolean = false,
|
var isFavorite: Boolean = false,
|
||||||
// 外部链接
|
|
||||||
val url: String? = null,
|
|
||||||
// 动态视频列表
|
|
||||||
val videos: List<MomentVideoEntity>? = null,
|
|
||||||
// 新闻相关字段
|
// 新闻相关字段
|
||||||
val isNews: Boolean = false,
|
val isNews: Boolean = false,
|
||||||
val newsTitle: String = "",
|
val newsTitle: String = "",
|
||||||
@@ -361,22 +307,13 @@ data class MomentEntity(
|
|||||||
val newsSource: String = "",
|
val newsSource: String = "",
|
||||||
val newsCategory: String = "",
|
val newsCategory: String = "",
|
||||||
val newsLanguage: String = "",
|
val newsLanguage: String = "",
|
||||||
val newsContent: String = "",
|
val newsContent: String = ""
|
||||||
// 是否已获取完整正文
|
|
||||||
val hasFullText: Boolean = false,
|
|
||||||
// 新闻摘要
|
|
||||||
val summary: String? = null,
|
|
||||||
// 新闻发布时间
|
|
||||||
val publishedAt: String? = null,
|
|
||||||
// 是否已缓存图片
|
|
||||||
val imageCached: Boolean = false
|
|
||||||
)
|
)
|
||||||
class MomentLoaderExtraArgs(
|
class MomentLoaderExtraArgs(
|
||||||
val explore: Boolean? = false,
|
val explore: Boolean? = false,
|
||||||
val timelineId: Int? = null,
|
val timelineId: Int? = null,
|
||||||
val authorId : Int? = null,
|
val authorId : Int? = null,
|
||||||
val newsOnly: Boolean? = null,
|
val newsOnly: Boolean? = null
|
||||||
val videoOnly: Boolean? = null
|
|
||||||
)
|
)
|
||||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
@@ -390,8 +327,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
explore = if (extra.explore == true) "true" else "",
|
explore = if (extra.explore == true) "true" else "",
|
||||||
timelineId = extra.timelineId,
|
timelineId = extra.timelineId,
|
||||||
authorId = extra.authorId,
|
authorId = extra.authorId,
|
||||||
newsFilter = if (extra.newsOnly == true) "news_only" else "",
|
newsFilter = if (extra.newsOnly == true) "news_only" else ""
|
||||||
videoFilter = if (extra.videoOnly == true) "video_only" else ""
|
|
||||||
)
|
)
|
||||||
val data = result.body()?.let {
|
val data = result.body()?.let {
|
||||||
ListContainer(
|
ListContainer(
|
||||||
@@ -410,16 +346,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
fun updateMomentLike(id: Int,isLike:Boolean) {
|
fun updateMomentLike(id: Int,isLike:Boolean) {
|
||||||
this.list = this.list.map { momentItem ->
|
this.list = this.list.map { momentItem ->
|
||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
|
||||||
val countDelta = if (momentItem.liked != isLike) {
|
|
||||||
if (isLike) 1 else -1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
momentItem.copy(
|
|
||||||
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
|
|
||||||
liked = isLike
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
}
|
}
|
||||||
@@ -430,16 +357,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
|
||||||
this.list = this.list.map { momentItem ->
|
this.list = this.list.map { momentItem ->
|
||||||
if (momentItem.id == id) {
|
if (momentItem.id == id) {
|
||||||
// 只有当状态发生变化时才更新计数,避免重复更新
|
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
|
||||||
val countDelta = if (momentItem.isFavorite != isFavorite) {
|
|
||||||
if (isFavorite) 1 else -1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
momentItem.copy(
|
|
||||||
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
|
|
||||||
isFavorite = isFavorite
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
momentItem
|
momentItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
package com.aiosman.ravenow.entity
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import androidx.paging.PagingState
|
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
import com.aiosman.ravenow.data.Room
|
|
||||||
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 java.io.IOException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 群聊房间
|
* 群聊房间
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* 房间内的智能体信息实体
|
|
||||||
*/
|
|
||||||
data class PromptTemplateEntity(
|
|
||||||
val id: Int,
|
|
||||||
val openId: String,
|
|
||||||
val title: String,
|
|
||||||
val desc: String,
|
|
||||||
val avatar: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RoomEntity(
|
data class RoomEntity(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -37,16 +21,9 @@ data class RoomEntity(
|
|||||||
val allowInHot: Boolean,
|
val allowInHot: Boolean,
|
||||||
val creator: CreatorEntity,
|
val creator: CreatorEntity,
|
||||||
val userCount: Int,
|
val userCount: Int,
|
||||||
val totalMemberCount: Int? = null,
|
|
||||||
val maxMemberLimit: Int,
|
val maxMemberLimit: Int,
|
||||||
val maxTotal: Int? = null,
|
|
||||||
val systemMaxTotal: Int? = null,
|
|
||||||
val canJoin: Boolean,
|
val canJoin: Boolean,
|
||||||
val canJoinCode: Int,
|
val canJoinCode: Int,
|
||||||
val privateFeePaid: Boolean = false,
|
|
||||||
val prompts: List<PromptTemplateEntity> = emptyList(),
|
|
||||||
val createdAt: String? = null,
|
|
||||||
val updatedAt: String? = null,
|
|
||||||
val users: List<UsersEntity>,
|
val users: List<UsersEntity>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,12 +58,7 @@ data class ProfileEntity(
|
|||||||
data class RoomRuleCreatorEntity(
|
data class RoomRuleCreatorEntity(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
val avatar: String,
|
val avatar: String
|
||||||
val avatarMedium: String? = null,
|
|
||||||
val avatarLarge: String? = null,
|
|
||||||
val avatarDirectUrl: String? = null,
|
|
||||||
val avatarMediumDirectUrl: String? = null,
|
|
||||||
val avatarLargeDirectUrl: String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,128 +86,6 @@ data class RoomRuleQuotaEntity(
|
|||||||
val usagePercent: Double
|
val usagePercent: Double
|
||||||
)
|
)
|
||||||
|
|
||||||
// ========== Room Member Management 实体类 ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户成功项目
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomItemEntity(
|
|
||||||
val userId: String,
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户失败项目
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomFailedItemEntity(
|
|
||||||
val userId: String,
|
|
||||||
val type: String,
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加用户到房间的结果
|
|
||||||
*/
|
|
||||||
data class AddUserToRoomResultEntity(
|
|
||||||
val totalCount: Int,
|
|
||||||
val successCount: Int,
|
|
||||||
val failedCount: Int,
|
|
||||||
val skippedCount: Int,
|
|
||||||
val successItems: List<AddUserToRoomItemEntity>,
|
|
||||||
val failedItems: List<AddUserToRoomFailedItemEntity>,
|
|
||||||
val skippedItems: List<AddUserToRoomItemEntity>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体成功项目
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomItemEntity(
|
|
||||||
val agentOpenId: String,
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体失败项目
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomFailedItemEntity(
|
|
||||||
val agentOpenId: String,
|
|
||||||
val type: String,
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加智能体到房间的结果
|
|
||||||
*/
|
|
||||||
data class AddAgentToRoomResultEntity(
|
|
||||||
val totalCount: Int,
|
|
||||||
val successCount: Int,
|
|
||||||
val failedCount: Int,
|
|
||||||
val skippedCount: Int,
|
|
||||||
val successItems: List<AddAgentToRoomItemEntity>,
|
|
||||||
val failedItems: List<AddAgentToRoomFailedItemEntity>,
|
|
||||||
val skippedItems: List<AddAgentToRoomItemEntity>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除智能体成功项目
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomItemEntity(
|
|
||||||
val agentOpenId: String,
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除智能体失败项目
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomFailedItemEntity(
|
|
||||||
val agentOpenId: String,
|
|
||||||
val type: String,
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除智能体的结果
|
|
||||||
*/
|
|
||||||
data class RemoveAgentFromRoomResultEntity(
|
|
||||||
val totalCount: Int,
|
|
||||||
val successCount: Int,
|
|
||||||
val failedCount: Int,
|
|
||||||
val skippedCount: Int,
|
|
||||||
val successItems: List<RemoveAgentFromRoomItemEntity>,
|
|
||||||
val failedItems: List<RemoveAgentFromRoomFailedItemEntity>,
|
|
||||||
val skippedItems: List<RemoveAgentFromRoomItemEntity>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除用户成功项目
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomItemEntity(
|
|
||||||
val userId: String,
|
|
||||||
val type: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除用户失败项目
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomFailedItemEntity(
|
|
||||||
val userId: String,
|
|
||||||
val type: String,
|
|
||||||
val error: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从房间移除用户的结果
|
|
||||||
*/
|
|
||||||
data class RemoveUserFromRoomResultEntity(
|
|
||||||
val totalCount: Int,
|
|
||||||
val successCount: Int,
|
|
||||||
val failedCount: Int,
|
|
||||||
val skippedCount: Int,
|
|
||||||
val successItems: List<RemoveUserFromRoomItemEntity>,
|
|
||||||
val failedItems: List<RemoveUserFromRoomFailedItemEntity>,
|
|
||||||
val skippedItems: List<RemoveUserFromRoomItemEntity>
|
|
||||||
)
|
|
||||||
|
|
||||||
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
page: Int,
|
page: Int,
|
||||||
@@ -263,94 +113,3 @@ class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 房间远程数据源
|
|
||||||
*/
|
|
||||||
class RoomRemoteDataSource {
|
|
||||||
suspend fun searchRooms(
|
|
||||||
pageNumber: Int,
|
|
||||||
pageSize: Int = 20,
|
|
||||||
search: String
|
|
||||||
): ListContainer<RoomEntity>? {
|
|
||||||
return try {
|
|
||||||
val resp = ApiClient.api.getRooms(
|
|
||||||
page = pageNumber,
|
|
||||||
pageSize = pageSize,
|
|
||||||
search = search,
|
|
||||||
roomType = "public" // 搜索时只显示公有房间
|
|
||||||
)
|
|
||||||
if (!resp.isSuccessful) {
|
|
||||||
// API 调用失败,返回 null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val body = resp.body() ?: return null
|
|
||||||
|
|
||||||
// 安全地转换数据,过滤掉转换失败的项目
|
|
||||||
val roomList = body.list.mapNotNull { room ->
|
|
||||||
try {
|
|
||||||
room.toRoomtEntity()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 如果某个房间数据转换失败,记录错误但继续处理其他房间
|
|
||||||
Log.e("RoomRemoteDataSource", "Failed to convert room: ${room.id}", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListContainer(
|
|
||||||
total = body.total,
|
|
||||||
page = pageNumber,
|
|
||||||
pageSize = pageSize,
|
|
||||||
list = roomList
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// 捕获所有异常,返回 null 让 PagingSource 处理
|
|
||||||
Log.e("RoomRemoteDataSource", "searchRooms error", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 房间搜索分页加载器
|
|
||||||
*/
|
|
||||||
class RoomSearchPagingSource(
|
|
||||||
private val roomRemoteDataSource: RoomRemoteDataSource,
|
|
||||||
private val keyword: String,
|
|
||||||
) : PagingSource<Int, RoomEntity>() {
|
|
||||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RoomEntity> {
|
|
||||||
return try {
|
|
||||||
val currentPage = params.key ?: 1
|
|
||||||
val rooms = roomRemoteDataSource.searchRooms(
|
|
||||||
pageNumber = currentPage,
|
|
||||||
pageSize = params.loadSize,
|
|
||||||
search = keyword
|
|
||||||
)
|
|
||||||
if (rooms == null) {
|
|
||||||
// API 调用失败,返回空列表
|
|
||||||
LoadResult.Page(
|
|
||||||
data = emptyList(),
|
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
|
||||||
nextKey = null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LoadResult.Page(
|
|
||||||
data = rooms.list,
|
|
||||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
|
||||||
nextKey = if (rooms.list.isNotEmpty() && rooms.list.size >= params.loadSize) currentPage + 1 else null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
// 捕获所有异常,包括 IOException、ServiceException 等
|
|
||||||
LoadResult.Error(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
|
||||||
// 更健壮的实现:根据 anchorPosition 计算刷新键
|
|
||||||
return state.anchorPosition?.let { anchorPosition ->
|
|
||||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
|
||||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ fun Date.formatPostTime(): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* yyyy-MM-dd HH:mm
|
* YYYY.DD.MM HH:MM
|
||||||
*/
|
*/
|
||||||
fun Date.formatPostTime2(): String {
|
fun Date.formatPostTime2(): String {
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
@@ -58,14 +58,7 @@ fun Date.formatPostTime2(): String {
|
|||||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||||
val hour = calendar.get(Calendar.HOUR_OF_DAY)
|
val hour = calendar.get(Calendar.HOUR_OF_DAY)
|
||||||
val minute = calendar.get(Calendar.MINUTE)
|
val minute = calendar.get(Calendar.MINUTE)
|
||||||
|
return "$year.$month.$day $hour:$minute"
|
||||||
// 确保两位数
|
|
||||||
val monthStr = String.format("%02d", month)
|
|
||||||
val dayStr = String.format("%02d", day)
|
|
||||||
val hourStr = String.format("%02d", hour)
|
|
||||||
val minuteStr = String.format("%02d", minute)
|
|
||||||
|
|
||||||
return "$year-$monthStr-$dayStr $hourStr:$minuteStr"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Date.formatChatTime(context: Context): String {
|
fun Date.formatChatTime(context: Context): String {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,16 +24,11 @@ object AppStore {
|
|||||||
.requestEmail()
|
.requestEmail()
|
||||||
.build()
|
.build()
|
||||||
googleSignInOptions = gso
|
googleSignInOptions = gso
|
||||||
// apply dark mode - 如果用户未手动设置,优先跟随系统
|
// apply dark mode
|
||||||
val hasUserPreference = sharedPreferences.contains("darkMode")
|
if (sharedPreferences.getBoolean("darkMode", false)) {
|
||||||
val resolvedDarkMode = if (hasUserPreference) {
|
AppState.darkMode = true
|
||||||
sharedPreferences.getBoolean("darkMode", false)
|
AppState.appTheme = DarkThemeColors()
|
||||||
} else {
|
|
||||||
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
||||||
currentNightMode == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
}
|
}
|
||||||
AppState.darkMode = resolvedDarkMode
|
|
||||||
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
|
|
||||||
|
|
||||||
// load chat background
|
// load chat background
|
||||||
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ 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.BlockedUsersScreen
|
|
||||||
import com.aiosman.ravenow.ui.account.MbtiSelectScreen
|
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.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.agent.AiPromptEditScreen
|
|
||||||
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.ChatSettingScreen
|
||||||
@@ -61,9 +59,6 @@ import com.aiosman.ravenow.ui.gallery.OfficialGalleryScreen
|
|||||||
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
|
import com.aiosman.ravenow.ui.gallery.OfficialPhotographerScreen
|
||||||
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
|
import com.aiosman.ravenow.ui.gallery.ProfileTimelineScreen
|
||||||
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
|
import com.aiosman.ravenow.ui.group.GroupChatInfoScreen
|
||||||
import com.aiosman.ravenow.ui.group.GroupMembersScreen
|
|
||||||
import com.aiosman.ravenow.ui.group.AddGroupMemberScreen
|
|
||||||
import com.aiosman.ravenow.ui.group.GroupProfileSettingsScreen
|
|
||||||
import com.aiosman.ravenow.ui.index.IndexScreen
|
import com.aiosman.ravenow.ui.index.IndexScreen
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
import com.aiosman.ravenow.ui.index.tabs.message.NotificationsScreen
|
||||||
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchScreen
|
||||||
@@ -78,10 +73,8 @@ import com.aiosman.ravenow.ui.post.NewPostImageGridScreen
|
|||||||
import com.aiosman.ravenow.ui.post.NewPostScreen
|
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.profile.AiProfileWrap
|
|
||||||
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
|
import com.aiosman.ravenow.ui.notification.NotificationScreen
|
||||||
import com.aiosman.ravenow.ui.scan.ScanQrScreen
|
|
||||||
|
|
||||||
sealed class NavigationRoute(
|
sealed class NavigationRoute(
|
||||||
val route: String,
|
val route: String,
|
||||||
@@ -126,17 +119,11 @@ sealed class NavigationRoute(
|
|||||||
data object AddAgent : NavigationRoute("AddAgent")
|
data object AddAgent : NavigationRoute("AddAgent")
|
||||||
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
|
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
|
||||||
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
|
||||||
data object GroupMembers : NavigationRoute("GroupMembers/{id}")
|
|
||||||
data object AddGroupMember : NavigationRoute("AddGroupMember/{groupId}/{groupName}")
|
|
||||||
data object GroupProfileSettings : NavigationRoute("GroupProfileSettings/{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 NotificationScreen : NavigationRoute("NotificationScreen")
|
||||||
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
||||||
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
||||||
data object ScanQr : NavigationRoute("ScanQr")
|
|
||||||
data object AiPromptEdit : NavigationRoute("AiPromptEdit/{chatAIId}")
|
|
||||||
data object BlockedUsersScreen : NavigationRoute("BlockedUsersScreen")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -350,15 +337,9 @@ fun NavigationController(
|
|||||||
) {
|
) {
|
||||||
val id = it.arguments?.getString("id")!!
|
val id = it.arguments?.getString("id")!!
|
||||||
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
|
val isAiAccount = it.arguments?.getBoolean("isAiAccount") ?: false
|
||||||
|
|
||||||
// 根据isAiAccount参数分发到不同的Profile页面
|
|
||||||
if (isAiAccount) {
|
|
||||||
AiProfileWrap(id)
|
|
||||||
} else {
|
|
||||||
AccountProfileV2(id, isAiAccount)
|
AccountProfileV2(id, isAiAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.SignUp.route,
|
route = NavigationRoute.SignUp.route,
|
||||||
enterTransition = {
|
enterTransition = {
|
||||||
@@ -441,9 +422,6 @@ fun NavigationController(
|
|||||||
composable(route = NavigationRoute.ChangePasswordScreen.route) {
|
composable(route = NavigationRoute.ChangePasswordScreen.route) {
|
||||||
ChangePasswordScreen()
|
ChangePasswordScreen()
|
||||||
}
|
}
|
||||||
composable(route = NavigationRoute.BlockedUsersScreen.route) {
|
|
||||||
BlockedUsersScreen()
|
|
||||||
}
|
|
||||||
composable(route = NavigationRoute.RemoveAccountScreen.route) {
|
composable(route = NavigationRoute.RemoveAccountScreen.route) {
|
||||||
RemoveAccountScreen()
|
RemoveAccountScreen()
|
||||||
}
|
}
|
||||||
@@ -469,9 +447,6 @@ fun NavigationController(
|
|||||||
SearchScreen()
|
SearchScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(route = NavigationRoute.ScanQr.route) {
|
|
||||||
ScanQrScreen()
|
|
||||||
}
|
|
||||||
composable(
|
composable(
|
||||||
route = NavigationRoute.FollowerList.route,
|
route = NavigationRoute.FollowerList.route,
|
||||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||||
@@ -638,51 +613,6 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
|
||||||
route = NavigationRoute.GroupMembers.route,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
|
||||||
) {
|
|
||||||
val encodedId = it.arguments?.getString("id")
|
|
||||||
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
GroupMembersScreen(decodedId ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
route = NavigationRoute.AddGroupMember.route,
|
|
||||||
arguments = listOf(
|
|
||||||
navArgument("groupId") { type = NavType.StringType },
|
|
||||||
navArgument("groupName") { type = NavType.StringType }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
val encodedGroupId = it.arguments?.getString("groupId")
|
|
||||||
val decodedGroupId = encodedGroupId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
|
||||||
val encodedGroupName = it.arguments?.getString("groupName")
|
|
||||||
val decodedGroupName = encodedGroupName?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
AddGroupMemberScreen(decodedGroupId ?: "", decodedGroupName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(
|
|
||||||
route = NavigationRoute.GroupProfileSettings.route,
|
|
||||||
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
|
||||||
) {
|
|
||||||
val encodedId = it.arguments?.getString("id")
|
|
||||||
val decodedId = encodedId?.let { java.net.URLDecoder.decode(it, "UTF-8") }
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
GroupProfileSettingsScreen(decodedId ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
composable(route = NavigationRoute.NotificationScreen.route) {
|
composable(route = NavigationRoute.NotificationScreen.route) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAnimatedContentScope provides this,
|
LocalAnimatedContentScope provides this,
|
||||||
@@ -691,18 +621,6 @@ fun NavigationController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
|
||||||
route = NavigationRoute.AiPromptEdit.route,
|
|
||||||
arguments = listOf(navArgument("chatAIId") { type = NavType.StringType })
|
|
||||||
) {
|
|
||||||
val chatAIId = it.arguments?.getString("chatAIId") ?: ""
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalAnimatedContentScope provides this,
|
|
||||||
) {
|
|
||||||
AiPromptEditScreen(chatAIId = chatAIId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -783,34 +701,6 @@ fun NavHostController.navigateToGroupInfo(id: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavHostController.navigateToGroupMembers(id: String) {
|
|
||||||
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
route = NavigationRoute.GroupMembers.route
|
|
||||||
.replace("{id}", encodedId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavHostController.navigateToAddGroupMember(groupId: String, groupName: String?) {
|
|
||||||
val encodedGroupId = java.net.URLEncoder.encode(groupId, "UTF-8")
|
|
||||||
val encodedGroupName = java.net.URLEncoder.encode(groupName ?: "", "UTF-8")
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
route = NavigationRoute.AddGroupMember.route
|
|
||||||
.replace("{groupId}", encodedGroupId)
|
|
||||||
.replace("{groupName}", encodedGroupName)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavHostController.navigateToGroupProfileSettings(id: String) {
|
|
||||||
val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
|
|
||||||
navigate(
|
|
||||||
route = NavigationRoute.GroupProfileSettings.route
|
|
||||||
.replace("{id}", encodedId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.about
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -41,21 +40,22 @@ fun AboutScreen() {
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
NoticeScreenHeader(
|
NoticeScreenHeader(
|
||||||
title = stringResource(R.string.about_paipai),
|
title = stringResource(R.string.about_rave_now),
|
||||||
moreIcon = false
|
moreIcon = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.padding(start = 24.dp),
|
||||||
verticalArrangement = Arrangement.Center
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// app icondww
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
// app icon
|
||||||
Box {
|
Box {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.invalid_name),
|
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
|
||||||
contentDescription = "app icon",
|
contentDescription = "app icon",
|
||||||
modifier = Modifier.size(80.dp)
|
modifier = Modifier.size(80.dp)
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ fun AboutScreen() {
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
// app name
|
// app name
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.paipai),
|
text = "Rave Now".uppercase(),
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
@@ -71,7 +71,7 @@ fun AboutScreen() {
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
// app version
|
// app version
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.version_text, versionText ?: ""),
|
text = stringResource(R.string.version_text, versionText),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = appColors.secondaryText,
|
color = appColors.secondaryText,
|
||||||
fontWeight = FontWeight.Normal
|
fontWeight = FontWeight.Normal
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ 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)
|
||||||
@@ -31,12 +29,7 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
// 本地扩展字段
|
// 本地扩展字段
|
||||||
var mbti by mutableStateOf<String?>(null)
|
var mbti by mutableStateOf<String?>(null)
|
||||||
var zodiac by mutableStateOf<String?>(null)
|
var zodiac by mutableStateOf<String?>(null)
|
||||||
// 保存原始值,用于取消时恢复
|
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
|
||||||
private var originalName: String = ""
|
|
||||||
private var originalBio: String = ""
|
|
||||||
private var originalMbti: String? = null
|
|
||||||
private var originalZodiac: String? = null
|
|
||||||
suspend fun reloadProfile(updateTrtcProfile:Boolean = false, clearCroppedBitmap: Boolean = false) {
|
|
||||||
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
|
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
|
||||||
isLoading = true
|
isLoading = true
|
||||||
try {
|
try {
|
||||||
@@ -46,23 +39,13 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
profile = it
|
profile = it
|
||||||
name = it.nickName
|
name = it.nickName
|
||||||
bio = it.bio
|
bio = it.bio
|
||||||
// 保存原始值,用于取消时恢复
|
// 清除之前裁剪的图片
|
||||||
originalName = it.nickName
|
|
||||||
originalBio = it.bio
|
|
||||||
// 只在明确要求时清除之前裁剪的图片(例如保存成功后)
|
|
||||||
if (clearCroppedBitmap) {
|
|
||||||
croppedBitmap = null
|
croppedBitmap = null
|
||||||
}
|
|
||||||
// 读取本地扩展字段
|
// 读取本地扩展字段
|
||||||
try {
|
try {
|
||||||
val uid = it.id // 使用 profile 的 id,确保非空
|
val uid = it.id // 使用 profile 的 id,确保非空
|
||||||
val loadedMbti = com.aiosman.ravenow.AppStore.getUserMbti(uid)
|
mbti = com.aiosman.ravenow.AppStore.getUserMbti(uid)
|
||||||
val loadedZodiac = com.aiosman.ravenow.AppStore.getUserZodiac(uid)
|
zodiac = com.aiosman.ravenow.AppStore.getUserZodiac(uid)
|
||||||
mbti = loadedMbti
|
|
||||||
zodiac = loadedZodiac
|
|
||||||
// 保存原始值
|
|
||||||
originalMbti = loadedMbti
|
|
||||||
originalZodiac = loadedZodiac
|
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
if (updateTrtcProfile) {
|
if (updateTrtcProfile) {
|
||||||
TrtcHelper.updateTrtcProfile(
|
TrtcHelper.updateTrtcProfile(
|
||||||
@@ -84,15 +67,12 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetToOriginalData() {
|
fun resetToOriginalData() {
|
||||||
// 恢复所有字段到原始值
|
profile?.let {
|
||||||
name = originalName
|
name = it.nickName
|
||||||
bio = originalBio
|
bio = it.bio
|
||||||
mbti = originalMbti
|
// 清除之前裁剪的图片
|
||||||
zodiac = originalZodiac
|
|
||||||
// 清除之前裁剪的图片和壁纸
|
|
||||||
croppedBitmap = null
|
croppedBitmap = null
|
||||||
bannerImageUrl = null
|
}
|
||||||
bannerFile = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -102,30 +82,6 @@ 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", "")
|
||||||
@@ -133,7 +89,7 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
val newName = if (cleanName == profile?.nickName) null else cleanName
|
val newName = if (cleanName == profile?.nickName) null else cleanName
|
||||||
accountService.updateProfile(
|
accountService.updateProfile(
|
||||||
avatar = newAvatar,
|
avatar = newAvatar,
|
||||||
banner = newBanner,
|
banner = null,
|
||||||
nickName = newName,
|
nickName = newName,
|
||||||
bio = cleanBio
|
bio = cleanBio
|
||||||
)
|
)
|
||||||
@@ -144,11 +100,8 @@ object AccountEditViewModel : ViewModel() {
|
|||||||
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
|
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiac)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
// 清除背景图状态
|
// 刷新用户资料
|
||||||
bannerImageUrl = null
|
reloadProfile()
|
||||||
bannerFile = null
|
|
||||||
// 刷新用户资料,保存成功后清除裁剪的图片
|
|
||||||
reloadProfile(clearCroppedBitmap = true)
|
|
||||||
// 刷新个人资料页面的用户资料
|
// 刷新个人资料页面的用户资料
|
||||||
MyProfileViewModel.loadUserProfile()
|
MyProfileViewModel.loadUserProfile()
|
||||||
}
|
}
|
||||||
@@ -163,8 +116,6 @@ 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
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ fun AccountSetting() {
|
|||||||
SecurityOptionItem(
|
SecurityOptionItem(
|
||||||
iconRes = R.mipmap.icons_block,
|
iconRes = R.mipmap.icons_block,
|
||||||
label = stringResource(R.string.blocked_users),
|
label = stringResource(R.string.blocked_users),
|
||||||
onClick = { navController.navigate(NavigationRoute.BlockedUsersScreen.route) }
|
onClick = {
|
||||||
|
// TODO: 导航到屏蔽用户页面
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SecurityOptionItem(
|
SecurityOptionItem(
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
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.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.LocalNavController
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
|
|
||||||
private object BlockedUsersConstants {
|
|
||||||
const val BACK_BUTTON_SIZE = 36
|
|
||||||
const val BACK_BUTTON_ICON_SIZE = 24
|
|
||||||
const val BACK_BUTTON_START_PADDING = 19
|
|
||||||
const val HEADER_VERTICAL_PADDING = 16
|
|
||||||
const val TITLE_OFFSET_X = 19
|
|
||||||
const val TITLE_TEXT_SIZE = 17
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CircularBackButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = "返回",
|
|
||||||
modifier = modifier
|
|
||||||
.size(BlockedUsersConstants.BACK_BUTTON_ICON_SIZE.dp)
|
|
||||||
.noRippleClickable { onClick() },
|
|
||||||
colorFilter = ColorFilter.tint(appColors.text)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 被屏蔽的用户界面
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun BlockedUsersScreen() {
|
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(appColors.background),
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
|
|
||||||
// 顶部标题栏
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = BlockedUsersConstants.HEADER_VERTICAL_PADDING.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
CircularBackButton(
|
|
||||||
onClick = { navController.navigateUp() },
|
|
||||||
modifier = Modifier.padding(start = BlockedUsersConstants.BACK_BUTTON_START_PADDING.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.blocked_users),
|
|
||||||
fontWeight = FontWeight.W800,
|
|
||||||
fontSize = BlockedUsersConstants.TITLE_TEXT_SIZE.sp,
|
|
||||||
color = appColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缺省状态
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(top = 149.dp),
|
|
||||||
contentAlignment = Alignment.TopCenter
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.mipmap.frame_23),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(181.dp, 153.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(9.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_users_isolated_yet),
|
|
||||||
color = appColors.text,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MbtiBottomSheetHost() {
|
|
||||||
val show = MbtiSheetManager.visible.collectAsState(false).value
|
|
||||||
if (show) {
|
|
||||||
MbtiSelectBottomSheet(
|
|
||||||
onClose = { MbtiSheetManager.close() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +1,42 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
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.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
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.draw.shadow
|
|
||||||
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.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
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.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
|
||||||
// MBTI类型列表
|
// MBTI类型列表
|
||||||
val MBTI_TYPES = listOf(
|
val MBTI_TYPES = listOf(
|
||||||
@@ -66,318 +46,96 @@ val MBTI_TYPES = listOf(
|
|||||||
"ISTP", "ISFP", "ESTP", "ESFP"
|
"ISTP", "ISFP", "ESTP", "ESFP"
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
|
|
||||||
return when {
|
|
||||||
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
|
|
||||||
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
|
|
||||||
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
|
|
||||||
else -> when (mbti) {
|
|
||||||
"INTJ" -> R.mipmap.mbti_intj
|
|
||||||
"INTP" -> R.mipmap.mbti_intp
|
|
||||||
"ENTJ" -> R.mipmap.mbti_entj
|
|
||||||
"ENTP" -> R.mipmap.mbti_entp
|
|
||||||
"INFJ" -> R.mipmap.mbti_infj
|
|
||||||
"INFP" -> R.mipmap.mbti_infp
|
|
||||||
"ENFJ" -> R.mipmap.mbti_enfj
|
|
||||||
"ENFP" -> R.mipmap.mbti_enfp
|
|
||||||
"ISTJ" -> R.mipmap.mbti_istj
|
|
||||||
"ISFJ" -> R.mipmap.mbti_isfj
|
|
||||||
"ESTJ" -> R.mipmap.mbti_estj
|
|
||||||
"ESFJ" -> R.mipmap.mbti_esfj
|
|
||||||
"ISTP" -> R.mipmap.mbti_istp
|
|
||||||
"ISFP" -> R.mipmap.mbti_isfp
|
|
||||||
"ESTP" -> R.mipmap.mbti_estp
|
|
||||||
"ESFP" -> R.mipmap.mbti_esfp
|
|
||||||
else -> R.mipmap.xingzuo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MbtiSelectBottomSheet(
|
fun MbtiSelectScreen() {
|
||||||
onClose: () -> Unit
|
val navController = LocalNavController.current
|
||||||
) {
|
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val currentMbti = model.mbti
|
val currentMbti = model.mbti
|
||||||
val sheetBackgroundColor = if (isDarkMode) {
|
|
||||||
appColors.secondaryBackground
|
|
||||||
} else {
|
|
||||||
Color(0xFFFFFFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onClose,
|
|
||||||
sheetState = sheetState,
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
|
||||||
dragHandle = {}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
|
||||||
color = sheetBackgroundColor,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight()
|
.background(appColors.profileBackground)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
// 头部
|
// 头部
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(48.dp),
|
.padding(vertical = 16.dp)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
val cancelButtonGradientColors = if (isDarkMode) {
|
NoticeScreenHeader(
|
||||||
listOf(
|
title = stringResource(R.string.choose_mbti),
|
||||||
Color(0xFF3A3A3C),
|
moreIcon = false
|
||||||
Color(0xFF2C2C2E)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
Color(0xFFFFFFFF),
|
|
||||||
Color(0xFFF8F8F8)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
|
||||||
|
|
||||||
// 左上角返回按钮:整体「箭头 + 取消」在按钮内居中
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.width(91.dp)
|
|
||||||
.height(44.dp)
|
|
||||||
.clip(RoundedCornerShape(1000.dp))
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = cancelButtonGradientColors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.noRippleClickable { onClose() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(17.dp),
|
|
||||||
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "取消",
|
|
||||||
fontSize = 17.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = cancelButtonContentColor,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Clip
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中间标题
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.choose_mbti),
|
|
||||||
color = appColors.text,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
// 列表
|
||||||
|
|
||||||
// NestedScroll,阻止滚动事件向上传到 BottomSheet
|
|
||||||
val nestedScrollConnection = remember {
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
return Offset.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostScroll(
|
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset {
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
|
||||||
return Velocity.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val descriptionBackgroundColor = if (isDarkMode) {
|
|
||||||
Color(0xFF2A2A2A)
|
|
||||||
} else {
|
|
||||||
Color(0xFFFAF9FB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 列表:上面是说明文字,下面是 MBTI 网格
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxWidth()
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.weight(1f)
|
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
start = 8.dp,
|
|
||||||
top = 0.dp,
|
|
||||||
end = 8.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
// 说明文字
|
items(MBTI_TYPES) { mbti ->
|
||||||
item {
|
MBTIItem(
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(descriptionBackgroundColor)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.mbti_description),
|
|
||||||
color = appColors.text,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
lineHeight = 20.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MBTI 类型网格(2 列)
|
|
||||||
itemsIndexed(MBTI_TYPES.chunked(2)) { rowIndex, rowItems ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
bottom = if (rowIndex < MBTI_TYPES.chunked(2).size - 1) 10.dp else 0.dp
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
|
||||||
rowItems.forEach { mbti ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
MbtiItem(
|
|
||||||
mbti = mbti,
|
mbti = mbti,
|
||||||
isSelected = mbti == currentMbti,
|
isSelected = mbti == currentMbti,
|
||||||
onClick = {
|
onClick = {
|
||||||
model.mbti = mbti
|
model.mbti = mbti
|
||||||
onClose()
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowItems.size == 1) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MbtiSelectScreen() {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
MbtiSelectBottomSheet(
|
|
||||||
onClose = {
|
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MbtiItem(
|
fun MBTIItem(
|
||||||
mbti: String,
|
mbti: String,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
|
|
||||||
// 卡片背景色
|
Box(
|
||||||
val cardBackgroundColor = if (isDarkMode) {
|
|
||||||
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
|
||||||
} else {
|
|
||||||
Color(0xFFFAF9FB)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1.1f)
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.shadow(
|
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
|
||||||
elevation = if (isDarkMode) 8.dp else 2.dp,
|
|
||||||
shape = RoundedCornerShape(21.dp),
|
|
||||||
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(21.dp))
|
|
||||||
.background(cardBackgroundColor)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(16.dp)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
) {
|
||||||
verticalArrangement = Arrangement.Center
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 直接把 MBTI 图标和文字放在灰色卡片内部,布局与星座保持一致
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
|
|
||||||
contentDescription = mbti,
|
|
||||||
modifier = Modifier.size(96.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = mbti,
|
text = mbti,
|
||||||
fontSize = 14.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text,
|
color = if (isSelected) appColors.main else appColors.text,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier.weight(1f)
|
||||||
modifier = Modifier.offset(y = (-10).dp)
|
)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Selected",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = appColors.main
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
object MbtiSheetManager {
|
|
||||||
private val _visible = MutableStateFlow(false)
|
|
||||||
val visible: StateFlow<Boolean> = _visible.asStateFlow()
|
|
||||||
|
|
||||||
fun open() {
|
|
||||||
_visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
_visible.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -20,14 +20,12 @@ 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.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
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.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.data.api.ErrorCode
|
import com.aiosman.ravenow.data.api.ErrorCode
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
@@ -101,11 +99,10 @@ fun ResetPasswordScreen() {
|
|||||||
if (e.code == ErrorCode.USER_NOT_EXIST.code){
|
if (e.code == ErrorCode.USER_NOT_EXIST.code){
|
||||||
usernameError = context.getString(R.string.error_40002_user_not_exist)
|
usernameError = context.getString(R.string.error_40002_user_not_exist)
|
||||||
} else {
|
} else {
|
||||||
// 其他错误,不显示Toast
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
isSendSuccess = false
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// 异常错误,不显示Toast
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
isSendSuccess = false
|
isSendSuccess = false
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@@ -136,21 +133,12 @@ fun ResetPasswordScreen() {
|
|||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 暗色模式下的 hint 文本颜色
|
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val hintColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
TextInputField(
|
TextInputField(
|
||||||
text = username,
|
text = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
hint = stringResource(R.string.text_hint_email),
|
hint = stringResource(R.string.text_hint_email),
|
||||||
enabled = !isLoading && countDown == null,
|
enabled = !isLoading && countDown == null,
|
||||||
error = usernameError,
|
error = usernameError,
|
||||||
customHintColor = hintColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Box(
|
Box(
|
||||||
@@ -190,11 +178,9 @@ fun ResetPasswordScreen() {
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.recover)
|
stringResource(R.string.recover)
|
||||||
},
|
},
|
||||||
backgroundColor = Color(0xFF7C45ED), // 紫色背景
|
backgroundColor = appColors.main,
|
||||||
loadingBackgroundColor = Color(0xFF7C45ED), // loading 时保持紫色
|
|
||||||
disabledBackgroundColor = Color(0xFF7C45ED), // disabled 时保持紫色
|
|
||||||
color = appColors.mainText,
|
color = appColors.mainText,
|
||||||
isLoading = isLoading && countDown == null, // 只在未发送成功时显示loading
|
isLoading = isLoading,
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
enabled = countDown == null,
|
enabled = countDown == null,
|
||||||
) {
|
) {
|
||||||
@@ -207,8 +193,6 @@ fun ResetPasswordScreen() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(48.dp),
|
.height(48.dp),
|
||||||
text = stringResource(R.string.back_upper),
|
text = stringResource(R.string.back_upper),
|
||||||
backgroundColor = Color(0xFF7C45ED), // 紫色背景
|
|
||||||
color = Color.White, // 白色文字
|
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ZodiacBottomSheetHost() {
|
|
||||||
val show = ZodiacSheetManager.visible.collectAsState(false).value
|
|
||||||
if (show) {
|
|
||||||
ZodiacSelectBottomSheet(
|
|
||||||
onClose = { ZodiacSheetManager.close() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,66 +1,39 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
package com.aiosman.ravenow.ui.account
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
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.layout.size
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
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.remember
|
||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
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.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.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
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.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import com.aiosman.ravenow.AppState
|
import com.aiosman.ravenow.AppState
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||||
|
|
||||||
// 星座资源ID列表
|
// 星座资源ID列表
|
||||||
val ZODIAC_SIGN_RES_IDS = listOf(
|
val ZODIAC_SIGN_RES_IDS = listOf(
|
||||||
@@ -78,27 +51,6 @@ val ZODIAC_SIGN_RES_IDS = listOf(
|
|||||||
R.string.zodiac_pisces
|
R.string.zodiac_pisces
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据星座资源ID获取对应的图片资源ID
|
|
||||||
*/
|
|
||||||
fun getZodiacImageResId(zodiacResId: Int): Int {
|
|
||||||
return when (zodiacResId) {
|
|
||||||
R.string.zodiac_aries -> R.mipmap.baiyang
|
|
||||||
R.string.zodiac_taurus -> R.mipmap.jingniu
|
|
||||||
R.string.zodiac_gemini -> R.mipmap.shuangzi
|
|
||||||
R.string.zodiac_cancer -> R.mipmap.juxie
|
|
||||||
R.string.zodiac_leo -> R.mipmap.shizi
|
|
||||||
R.string.zodiac_virgo -> R.mipmap.chunv
|
|
||||||
R.string.zodiac_libra -> R.mipmap.tiancheng
|
|
||||||
R.string.zodiac_scorpio -> R.mipmap.tianxie
|
|
||||||
R.string.zodiac_sagittarius -> R.mipmap.sheshou
|
|
||||||
R.string.zodiac_capricorn -> R.mipmap.moxie
|
|
||||||
R.string.zodiac_aquarius -> R.mipmap.shuiping
|
|
||||||
R.string.zodiac_pisces -> R.mipmap.shuangyu
|
|
||||||
else -> R.mipmap.xingzuo // 默认使用占位图片
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据存储的星座字符串(可能是任何语言)找到对应的资源ID
|
* 根据存储的星座字符串(可能是任何语言)找到对应的资源ID
|
||||||
* 如果找不到,返回null
|
* 如果找不到,返回null
|
||||||
@@ -120,198 +72,56 @@ fun findZodiacResId(storedZodiac: String?): Int? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ZodiacSelectBottomSheet(
|
fun ZodiacSelectScreen() {
|
||||||
onClose: () -> Unit
|
val navController = LocalNavController.current
|
||||||
) {
|
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val model = AccountEditViewModel
|
val model = AccountEditViewModel
|
||||||
val currentZodiacResId = findZodiacResId(model.zodiac)
|
val currentZodiacResId = findZodiacResId(model.zodiac)
|
||||||
val sheetBackgroundColor = if (isDarkMode) {
|
|
||||||
appColors.secondaryBackground
|
|
||||||
} else {
|
|
||||||
Color(0xFFFFFFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onClose,
|
|
||||||
sheetState = sheetState,
|
|
||||||
// 对齐发布动态草稿箱样式:底层透明,内容区域自己绘制圆角和背景
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
|
||||||
dragHandle = {}
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
|
|
||||||
color = sheetBackgroundColor,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight()
|
.background(appColors.profileBackground)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
// 头部 - 使用 Box 实现绝对居中布局(对齐草稿箱样式)
|
// 头部
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(48.dp),
|
.padding(vertical = 16.dp)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
val cancelButtonGradientColors = if (isDarkMode) {
|
NoticeScreenHeader(
|
||||||
listOf(
|
title = stringResource(R.string.choose_zodiac),
|
||||||
Color(0xFF3A3A3C),
|
moreIcon = false
|
||||||
Color(0xFF2C2C2E)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
Color(0xFFFFFFFF),
|
|
||||||
Color(0xFFF8F8F8)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
|
|
||||||
|
|
||||||
// 左上角返回按钮:参考 iOS 设计,整体「箭头 + 取消」在 91x44 的按钮内居中
|
// 列表
|
||||||
Box(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.align(Alignment.CenterStart)
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.width(91.dp)
|
|
||||||
.height(44.dp)
|
|
||||||
.clip(RoundedCornerShape(1000.dp))
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = cancelButtonGradientColors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.noRippleClickable { onClose() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Row(
|
items(ZODIAC_SIGN_RES_IDS.size) { index ->
|
||||||
modifier = Modifier
|
val zodiacResId = ZODIAC_SIGN_RES_IDS[index]
|
||||||
// 不再固定宽度,让内容自然占位,避免裁剪掉“消”字
|
|
||||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(17.dp),
|
|
||||||
colorFilter = ColorFilter.tint(cancelButtonContentColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "取消",
|
|
||||||
fontSize = 17.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = cancelButtonContentColor,
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Clip
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.choose_zodiac),
|
|
||||||
color = appColors.text,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// 创建 NestedScrollConnection:
|
|
||||||
// 1. 不抢在列表前面消费事件,让 LazyVerticalGrid 正常滚动
|
|
||||||
// 2. 在列表滚动之后把剩余滚动吃掉,避免继续传递到 BottomSheet 去触发下拉关闭
|
|
||||||
val nestedScrollConnection = remember {
|
|
||||||
object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
// 不在这里消费,先让 LazyVerticalGrid 自己处理滚动
|
|
||||||
return Offset.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
// 列表滚动完之后,把剩余滚动(尤其是向下拖拽)全部吃掉,防止再传给 BottomSheet
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
|
||||||
// 不抢在列表前面处理 fling,让 LazyVerticalGrid 先做惯性滚动
|
|
||||||
return Velocity.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
// 列表惯性滚动之后,把剩余的 fling 速度吃掉,避免带动 BottomSheet 下滑关闭
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 网格列表 - 2列(与草稿箱一样放在内容区域内部滚动)
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(2),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
.nestedScroll(nestedScrollConnection),
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
start = 8.dp,
|
|
||||||
top = 8.dp,
|
|
||||||
end = 8.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(ZODIAC_SIGN_RES_IDS) { index, zodiacResId ->
|
|
||||||
val zodiacText = stringResource(zodiacResId)
|
val zodiacText = stringResource(zodiacResId)
|
||||||
ZodiacItem(
|
ZodiacItem(
|
||||||
zodiac = zodiacText,
|
zodiac = zodiacText,
|
||||||
zodiacResId = zodiacResId,
|
zodiacResId = zodiacResId,
|
||||||
isSelected = zodiacResId == currentZodiacResId,
|
isSelected = zodiacResId == currentZodiacResId,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// 保存当前语言的星座文本
|
||||||
model.zodiac = zodiacText
|
model.zodiac = zodiacText
|
||||||
onClose()
|
// 立即保存到本地存储,确保选择后立即生效
|
||||||
|
AppState.UserId?.let { uid ->
|
||||||
|
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有的 ZodiacSelectScreen 用于导航路由(如果需要)
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ZodiacSelectScreen() {
|
|
||||||
val navController = com.aiosman.ravenow.LocalNavController.current
|
|
||||||
ZodiacSelectBottomSheet(
|
|
||||||
onClose = {
|
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -322,55 +132,41 @@ fun ZodiacItem(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
|
|
||||||
// 卡片背景色:浅灰色 (250, 249, 251)
|
Box(
|
||||||
// 暗色模式下使用比背景色更亮的颜色,以形成对比
|
|
||||||
val cardBackgroundColor = if (isDarkMode) {
|
|
||||||
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
|
|
||||||
} else {
|
|
||||||
Color(0xFFFAF9FB)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1.1f)
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.shadow(
|
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
|
||||||
elevation = if (isDarkMode) 8.dp else 2.dp,
|
|
||||||
shape = RoundedCornerShape(21.dp),
|
|
||||||
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(21.dp))
|
|
||||||
.background(cardBackgroundColor)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(16.dp)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
) {
|
||||||
verticalArrangement = Arrangement.Center
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 直接把图标和文字放在灰色卡片内部,不再额外嵌套一层 Box
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = getZodiacImageResId(zodiacResId)),
|
|
||||||
contentDescription = zodiac,
|
|
||||||
// 图标稍微放大一些,让视觉更聚焦在星座图标上
|
|
||||||
modifier = Modifier.size(96.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(0.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = zodiac,
|
text = zodiac,
|
||||||
fontSize = 14.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text,
|
color = if (isSelected) appColors.main else appColors.text,
|
||||||
textAlign = TextAlign.Center,
|
modifier = Modifier.weight(1f)
|
||||||
modifier = Modifier.offset(y = (-10).dp) // 再整体向上偏移 5dp(共 10dp)
|
)
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Selected",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = appColors.main
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.account
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
object ZodiacSheetManager {
|
|
||||||
private val _visible = MutableStateFlow(false)
|
|
||||||
val visible: StateFlow<Boolean> = _visible.asStateFlow()
|
|
||||||
|
|
||||||
fun open() {
|
|
||||||
_visible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
_visible.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -16,11 +16,9 @@ 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.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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 com.aiosman.ravenow.AppState
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
@@ -70,19 +68,6 @@ fun ChangePasswordScreen() {
|
|||||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
// 暗色模式下的 hint 文本颜色
|
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val hintColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
val labelColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
// 使用通用密码校验器校验当前密码
|
// 使用通用密码校验器校验当前密码
|
||||||
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
||||||
@@ -127,9 +112,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.current_password),
|
label = stringResource(R.string.current_password),
|
||||||
hint = stringResource(R.string.current_password_tip5),
|
hint = stringResource(R.string.current_password_tip5),
|
||||||
error = oldPasswordError,
|
error = oldPasswordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextInputField(
|
TextInputField(
|
||||||
@@ -138,9 +121,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.new_password),
|
label = stringResource(R.string.new_password),
|
||||||
hint = stringResource(R.string.new_password),
|
hint = stringResource(R.string.new_password),
|
||||||
error = passwordError,
|
error = passwordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
TextInputField(
|
TextInputField(
|
||||||
@@ -149,9 +130,7 @@ fun ChangePasswordScreen() {
|
|||||||
password = true,
|
password = true,
|
||||||
label = stringResource(R.string.confirm_new_password_tip1),
|
label = stringResource(R.string.confirm_new_password_tip1),
|
||||||
hint = stringResource(R.string.new_password_tip1),
|
hint = stringResource(R.string.new_password_tip1),
|
||||||
error = confirmPasswordError,
|
error = confirmPasswordError
|
||||||
customHintColor = hintColor,
|
|
||||||
customLabelColor = labelColor
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(50.dp))
|
||||||
ActionButton(
|
ActionButton(
|
||||||
|
|||||||
@@ -70,17 +70,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import com.aiosman.ravenow.ConstVars
|
import com.aiosman.ravenow.ConstVars
|
||||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||||
import android.widget.Toast
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
|
|
||||||
import com.aiosman.ravenow.ui.account.ZodiacSheetManager
|
|
||||||
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
|
|
||||||
import com.aiosman.ravenow.ui.account.MbtiSheetManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑用户资料界面
|
* 编辑用户资料界面
|
||||||
@@ -105,10 +97,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
quality = 100
|
quality = 100
|
||||||
) { uri, file ->
|
) { uri, file ->
|
||||||
// 处理选中的图片
|
// 处理选中的图片
|
||||||
// 保存到 ViewModel 中,等待保存时一起上传
|
|
||||||
model.bannerImageUrl = uri
|
|
||||||
model.bannerFile = file
|
|
||||||
// 如果提供了回调,也调用它(用于个人主页直接更新)
|
|
||||||
onUpdateBanner?.invoke(uri, file, context)
|
onUpdateBanner?.invoke(uri, file, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +104,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
// 去除换行符,确保昵称不包含换行
|
// 去除换行符,确保昵称不包含换行
|
||||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||||
model.name = cleanValue
|
model.name = cleanValue
|
||||||
// 实时验证,但不显示错误(只在保存时显示)
|
|
||||||
usernameError = when {
|
usernameError = when {
|
||||||
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
|
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
|
||||||
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
|
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
|
||||||
@@ -125,37 +112,18 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateNickname(): String? {
|
|
||||||
val cleanValue = model.name.replace("\n", "").replace("\r", "")
|
|
||||||
return when {
|
|
||||||
cleanValue.trim().isEmpty() -> context.getString(R.string.error_nickname_empty)
|
|
||||||
cleanValue.length < 3 -> context.getString(R.string.error_nickname_too_short)
|
|
||||||
cleanValue.length > 20 -> context.getString(R.string.error_nickname_too_long)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
|
|
||||||
fun onBioChange(value: String) {
|
fun onBioChange(value: String) {
|
||||||
// 去除换行符,确保个人简介不包含换行
|
// 去除换行符,确保个人简介不包含换行
|
||||||
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 -> context.getString(R.string.error_bio_too_long)
|
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateBio(): String? {
|
|
||||||
val cleanValue = model.bio.replace("\n", "").replace("\r", "")
|
|
||||||
return when {
|
|
||||||
cleanValue.length > 100 -> context.getString(R.string.error_bio_too_long)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validate(): Boolean {
|
fun validate(): Boolean {
|
||||||
return usernameError == null && bioError == null
|
return usernameError == null && bioError == null
|
||||||
}
|
}
|
||||||
@@ -178,33 +146,21 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
model.reloadProfile()
|
model.reloadProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理系统返回键
|
// 设置状态栏为透明,使用浅色图标(因为顶部背景是深色图片)
|
||||||
BackHandler {
|
|
||||||
// 用户未保存直接返回,恢复所有字段到原始值
|
|
||||||
model.resetToOriginalData()
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置状态栏为透明,根据暗色模式决定图标颜色
|
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
modifier = Modifier.background(appColors.background),
|
modifier = Modifier.background(Color(0xFFFAF9FB)),
|
||||||
darkIcons = !AppState.darkMode, // 根据暗色模式决定图标颜色
|
darkIcons = false, // 浅色图标(白色),因为顶部背景是深色
|
||||||
maskBoxBackgroundColor = Color.Transparent
|
maskBoxBackgroundColor = Color.Transparent
|
||||||
) {
|
) {
|
||||||
// 挂载星座选择弹窗
|
|
||||||
ZodiacBottomSheetHost()
|
|
||||||
// 挂载MBTI选择弹窗
|
|
||||||
MbtiBottomSheetHost()
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(appColors.background)
|
.background(Color(0xFFFAF9FB))
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
model.isLoading -> {
|
model.isLoading -> {
|
||||||
@@ -223,8 +179,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// 顶部背景区域(圆角在底部,覆盖状态栏)
|
// 顶部背景区域(圆角在底部,覆盖状态栏)
|
||||||
// 优先显示新选择的背景图,如果没有则显示原有的背景图
|
val banner = model.profile?.banner
|
||||||
val banner = model.bannerImageUrl?.toString() ?: model.profile?.banner
|
|
||||||
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
val statusBarPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -275,7 +230,15 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
) {
|
) {
|
||||||
// 更换封面图标
|
// 更换封面图标
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.mipmap.fengm),
|
painter = painterResource(
|
||||||
|
id = if (AppState.darkMode) {
|
||||||
|
// TODO: 添加更换封面暗色模式图标
|
||||||
|
R.mipmap.frame_4 // 临时占位,需替换为实际图标
|
||||||
|
} else {
|
||||||
|
// TODO: 添加更换封面亮色模式图标
|
||||||
|
R.mipmap.fengm // 临时占位,需替换为实际图标
|
||||||
|
}
|
||||||
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
@@ -310,8 +273,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White.copy(alpha = 0.3f))
|
.background(Color.White.copy(alpha = 0.3f))
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
// 用户未保存直接返回,恢复所有字段到原始值
|
|
||||||
model.resetToOriginalData()
|
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
.align(Alignment.CenterStart),
|
.align(Alignment.CenterStart),
|
||||||
@@ -357,7 +318,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(96.dp)
|
.size(96.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.border(2.4.dp, appColors.background, CircleShape),
|
.border(2.4.dp, Color(0xFFFAF9FB), CircleShape),
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
@@ -377,7 +338,15 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.mipmap.bi),
|
painter = painterResource(
|
||||||
|
id = if (AppState.darkMode) {
|
||||||
|
// TODO: 添加编辑头像暗色模式图标
|
||||||
|
R.mipmap.frame_4 // 临时占位,需替换为实际图标
|
||||||
|
} else {
|
||||||
|
// TODO: 添加编辑头像亮色模式图标
|
||||||
|
R.mipmap.bi // 临时占位,需替换为实际图标
|
||||||
|
}
|
||||||
|
),
|
||||||
contentDescription = "Edit Avatar",
|
contentDescription = "Edit Avatar",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
@@ -398,7 +367,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
ProfileInfoCard(
|
ProfileInfoCard(
|
||||||
label = stringResource(R.string.nickname),
|
label = stringResource(R.string.nickname),
|
||||||
value = model.name,
|
value = model.name,
|
||||||
placeholder = stringResource(R.string.nickname_placeholder),
|
placeholder = "Value",
|
||||||
onValueChange = { onNicknameChange(it) },
|
onValueChange = { onNicknameChange(it) },
|
||||||
isMultiline = false
|
isMultiline = false
|
||||||
)
|
)
|
||||||
@@ -409,7 +378,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
ProfileInfoCard(
|
ProfileInfoCard(
|
||||||
label = stringResource(R.string.personal_intro),
|
label = stringResource(R.string.personal_intro),
|
||||||
value = model.bio,
|
value = model.bio,
|
||||||
placeholder = "",
|
placeholder = "Welcome to my fantiac word i will show you something about magic",
|
||||||
onValueChange = { onBioChange(it) },
|
onValueChange = { onBioChange(it) },
|
||||||
isMultiline = true
|
isMultiline = true
|
||||||
)
|
)
|
||||||
@@ -421,7 +390,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(appColors.secondaryBackground)
|
.background(Color.White)
|
||||||
) {
|
) {
|
||||||
// MBTI 类型
|
// MBTI 类型
|
||||||
ProfileSelectItem(
|
ProfileSelectItem(
|
||||||
@@ -431,7 +400,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
iconResDark = null, // TODO: 添加MBTI暗色模式图标
|
||||||
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
iconResLight = null, // TODO: 添加MBTI亮色模式图标
|
||||||
onClick = {
|
onClick = {
|
||||||
MbtiSheetManager.open()
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.MbtiSelect.route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -440,7 +411,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(0.3.dp)
|
.height(0.3.dp)
|
||||||
.background(appColors.divider)
|
.background(Color(0x41413C43).copy(alpha = 0.2f))
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -457,7 +428,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
iconResDark = R.mipmap.frame_4, // 星座暗色模式图标
|
iconResDark = R.mipmap.frame_4, // 星座暗色模式图标
|
||||||
iconResLight = R.mipmap.xingzuo, // 星座亮色模式图标
|
iconResLight = R.mipmap.xingzuo, // 星座亮色模式图标
|
||||||
onClick = {
|
onClick = {
|
||||||
ZodiacSheetManager.open()
|
debouncedNavigation {
|
||||||
|
navController.navigate(NavigationRoute.ZodiacSelect.route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -480,31 +453,12 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.debouncedClickable(
|
.debouncedClickable(
|
||||||
enabled = !model.isUpdating,
|
enabled = validate() && !model.isUpdating,
|
||||||
debounceTime = 1000L
|
debounceTime = 1000L
|
||||||
) {
|
) {
|
||||||
if (model.isUpdating) return@debouncedClickable
|
if (validate() && !model.isUpdating) {
|
||||||
|
|
||||||
// 点击保存时重新验证
|
|
||||||
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.viewModelScope.launch {
|
||||||
model.isUpdating = true
|
model.isUpdating = true
|
||||||
try {
|
|
||||||
model.updateUserProfile(context)
|
model.updateUserProfile(context)
|
||||||
model.viewModelScope.launch(Dispatchers.Main) {
|
model.viewModelScope.launch(Dispatchers.Main) {
|
||||||
debouncedNavigation {
|
debouncedNavigation {
|
||||||
@@ -512,16 +466,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
|
|||||||
}
|
}
|
||||||
model.isUpdating = false
|
model.isUpdating = false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
// 捕获所有异常,包括网络异常
|
|
||||||
model.viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.network_error_check_network),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
model.isUpdating = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -566,45 +510,30 @@ fun ProfileInfoCard(
|
|||||||
isMultiline: Boolean = false
|
isMultiline: Boolean = false
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
val appColors = LocalAppTheme.current
|
||||||
var isFocused by remember { mutableStateOf(false) }
|
|
||||||
var lineCount by remember { mutableStateOf(1) }
|
|
||||||
|
|
||||||
// 根据行数决定对齐方式:单行时居中,多行时顶部对齐
|
|
||||||
val verticalAlignment = if (isMultiline) {
|
|
||||||
if (lineCount <= 1) Alignment.CenterVertically else Alignment.Top
|
|
||||||
} else {
|
|
||||||
Alignment.CenterVertically
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp,个人简介66dp
|
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp,个人简介66dp
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(appColors.secondaryBackground),
|
.background(Color.White),
|
||||||
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
|
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
|
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
|
||||||
verticalAlignment = verticalAlignment
|
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 标签
|
// 标签
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(100.dp)
|
|
||||||
.height(if (isMultiline) 44.dp else 56.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text
|
color = Color.Black,
|
||||||
|
modifier = Modifier.width(100.dp)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
@@ -612,28 +541,12 @@ fun ProfileInfoCard(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
// 对于个人简介(isMultiline = true),当值为空且没有焦点时显示图标
|
if (value.isEmpty()) {
|
||||||
if (value.isEmpty() && isMultiline && !isFocused) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(44.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = appColors.secondaryText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (value.isEmpty() && !isMultiline && placeholder.isNotEmpty()) {
|
|
||||||
// 对于非多行输入框,仍然显示 placeholder 文字
|
|
||||||
Text(
|
Text(
|
||||||
text = placeholder,
|
text = placeholder,
|
||||||
fontSize = 17.sp,
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.secondaryText,
|
color = Color(0x993C3C43),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -641,24 +554,15 @@ fun ProfileInfoCard(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.onFocusChanged { focusState ->
|
|
||||||
isFocused = focusState.isFocused
|
|
||||||
},
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
fontSize = if (isMultiline) 15.sp else 17.sp,
|
fontSize = if (isMultiline) 15.sp else 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text
|
color = Color.Black
|
||||||
),
|
),
|
||||||
cursorBrush = SolidColor(appColors.text),
|
cursorBrush = SolidColor(Color.Black),
|
||||||
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
|
||||||
singleLine = !isMultiline,
|
singleLine = !isMultiline
|
||||||
onTextLayout = { textLayoutResult: TextLayoutResult ->
|
|
||||||
if (isMultiline) {
|
|
||||||
lineCount = textLayoutResult.lineCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,7 +581,6 @@ fun ProfileSelectItem(
|
|||||||
iconResDark: Int? = null,
|
iconResDark: Int? = null,
|
||||||
iconResLight: Int? = null
|
iconResLight: Int? = null
|
||||||
) {
|
) {
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -695,8 +598,7 @@ fun ProfileSelectItem(
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if (AppState.darkMode) {
|
id = if (AppState.darkMode) {
|
||||||
// 暗色模式下使用和亮色模式一样的图标
|
iconResDark ?: R.mipmap.frame_4 // 使用传入的暗色模式图标,或默认占位
|
||||||
iconResLight ?: iconResDark ?: R.mipmap.naoz
|
|
||||||
} else {
|
} else {
|
||||||
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
|
iconResLight ?: R.mipmap.naoz // 使用传入的亮色模式图标,或默认占位
|
||||||
}
|
}
|
||||||
@@ -710,7 +612,7 @@ fun ProfileSelectItem(
|
|||||||
text = label,
|
text = label,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.text
|
color = Color.Black
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,14 +624,14 @@ fun ProfileSelectItem(
|
|||||||
text = value,
|
text = value,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
color = appColors.secondaryText
|
color = Color(0x993C3C43)
|
||||||
)
|
)
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowForward,
|
imageVector = Icons.Default.ArrowForward,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(8.dp),
|
modifier = Modifier.size(8.dp),
|
||||||
tint = appColors.secondaryText
|
tint = Color(0x4D3C3C43)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ 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.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -51,14 +50,6 @@ fun RemoveAccountScreen() {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// 暗色模式下的 hint 文本颜色
|
|
||||||
val isDarkMode = AppState.darkMode
|
|
||||||
val hintColor = if (isDarkMode) {
|
|
||||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
|
||||||
} else {
|
|
||||||
null // 使用默认颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAccount(password: String) {
|
fun removeAccount(password: String) {
|
||||||
// 使用通用密码校验器
|
// 使用通用密码校验器
|
||||||
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||||
@@ -141,8 +132,7 @@ fun RemoveAccountScreen() {
|
|||||||
},
|
},
|
||||||
password = true,
|
password = true,
|
||||||
hint = stringResource(R.string.remove_account_password_hint),
|
hint = stringResource(R.string.remove_account_password_hint),
|
||||||
error = passwordError,
|
error = passwordError
|
||||||
customHintColor = hintColor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
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
|
||||||
@@ -35,11 +34,8 @@ 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.LocalDensity
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
|
||||||
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.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -569,27 +565,10 @@ fun AddAgentScreen() {
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.align(Alignment.Start)
|
.align(Alignment.Start)
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
|
||||||
val textMeasurer = rememberTextMeasurer()
|
|
||||||
val autoLabel = stringResource(R.string.create_agent_auto)
|
|
||||||
val measuredTextWidth = remember(autoLabel, textMeasurer) {
|
|
||||||
textMeasurer.measure(
|
|
||||||
text = AnnotatedString(autoLabel),
|
|
||||||
style = TextStyle(
|
|
||||||
color = appColors.text,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
fontSize = 14.sp
|
|
||||||
)
|
|
||||||
).size.width
|
|
||||||
}
|
|
||||||
val textWidthDp = with(density) { measuredTextWidth.toDp() }
|
|
||||||
val contentWidth = 24.dp + 18.dp + 8.dp + textWidthDp
|
|
||||||
val boxWidth = if (contentWidth > 140.dp) 250.dp else 140.dp
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Start)
|
.align(Alignment.Start)
|
||||||
.width(boxWidth)
|
.width(140.dp)
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = 10.dp,
|
elevation = 10.dp,
|
||||||
@@ -637,7 +616,7 @@ fun AddAgentScreen() {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = autoLabel,
|
text = stringResource(R.string.create_agent_auto),
|
||||||
color = appColors.text,
|
color = appColors.text,
|
||||||
fontWeight = FontWeight.W600,
|
fontWeight = FontWeight.W600,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import java.io.InputStream
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 专门用于智能体头像裁剪的页面
|
* 专门用于智能体头像裁剪的页面
|
||||||
* 支持创建和编辑两种模式
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentImageCropScreen() {
|
fun AgentImageCropScreen() {
|
||||||
@@ -72,14 +71,6 @@ fun AgentImageCropScreen() {
|
|||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
// 检查是否在编辑模式(通过检查是否有编辑ViewModel的实例)
|
|
||||||
val isEditMode = remember {
|
|
||||||
// 通过检查导航栈或使用其他方式判断
|
|
||||||
// 暂时使用一个简单的方法:检查AddAgentViewModel是否正在选择头像
|
|
||||||
// 如果不是,则可能是编辑模式
|
|
||||||
!AddAgentViewModel.isSelectingAvatar
|
|
||||||
}
|
|
||||||
|
|
||||||
val imagePickLauncher = rememberLauncherForActivityResult(
|
val imagePickLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent()
|
contract = ActivityResultContracts.GetContent()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
@@ -93,9 +84,7 @@ fun AgentImageCropScreen() {
|
|||||||
}
|
}
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
// 用户取消选择图片,重置标志
|
// 用户取消选择图片,重置标志
|
||||||
if (!isEditMode) {
|
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
}
|
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +122,7 @@ fun AgentImageCropScreen() {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
// 用户取消头像选择,重置标志
|
// 用户取消头像选择,重置标志
|
||||||
if (!isEditMode) {
|
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
}
|
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(Color.White)
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
@@ -150,12 +137,6 @@ fun AgentImageCropScreen() {
|
|||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (croppedBitmap != null) {
|
if (croppedBitmap != null) {
|
||||||
// 如果已经有裁剪结果,直接返回
|
// 如果已经有裁剪结果,直接返回
|
||||||
if (isEditMode) {
|
|
||||||
// 编辑模式:需要找到当前的编辑ViewModel实例
|
|
||||||
// 由于无法直接访问,我们使用一个全局状态或者通过其他方式传递
|
|
||||||
// 暂时先保存到AddAgentViewModel,编辑页面会检查
|
|
||||||
AddAgentViewModel.croppedBitmap = croppedBitmap
|
|
||||||
} else {
|
|
||||||
AddAgentViewModel.croppedBitmap = croppedBitmap
|
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||||
// 重置头像选择标志
|
// 重置头像选择标志
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
AddAgentViewModel.isSelectingAvatar = false
|
||||||
@@ -163,8 +144,6 @@ fun AgentImageCropScreen() {
|
|||||||
AddAgentViewModel.updateAgentAvatar(context)
|
AddAgentViewModel.updateAgentAvatar(context)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
navController.popBackStack()
|
|
||||||
} else {
|
} else {
|
||||||
// 进行裁剪
|
// 进行裁剪
|
||||||
imageCrop?.let {
|
imageCrop?.let {
|
||||||
|
|||||||
@@ -1,483 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.agent
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.SwitchDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
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.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import com.aiosman.ravenow.LocalNavController
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
|
||||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|
||||||
import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
|
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
|
||||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
|
||||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI Prompt 编辑页面
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun AiPromptEditScreen(
|
|
||||||
chatAIId: String,
|
|
||||||
viewModel: AiPromptEditViewModel = viewModel()
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
// 加载Prompt详情
|
|
||||||
LaunchedEffect(chatAIId) {
|
|
||||||
viewModel.loadPromptDetail(chatAIId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听头像裁剪结果(从AgentImageCropScreen返回)
|
|
||||||
LaunchedEffect(viewModel.isSelectingAvatar) {
|
|
||||||
if (!viewModel.isSelectingAvatar && AddAgentViewModel.croppedBitmap != null) {
|
|
||||||
// 从裁剪页面返回,检查是否有新的裁剪结果
|
|
||||||
viewModel.croppedBitmap = AddAgentViewModel.croppedBitmap
|
|
||||||
// 清空AddAgentViewModel的裁剪结果,避免影响创建页面
|
|
||||||
AddAgentViewModel.croppedBitmap = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
var showPrivacyConfirmDialog by remember { mutableStateOf(false) }
|
|
||||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
// 获取积分规则和余额
|
|
||||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
|
||||||
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
|
|
||||||
|
|
||||||
// 计算是否需要付费
|
|
||||||
val needsPayment = viewModel.needsPrivacyPayment()
|
|
||||||
val privacyCost = viewModel.getPrivacyCost()
|
|
||||||
val currentBalance = viewModel.getCurrentBalance()
|
|
||||||
val balanceAfterCost = viewModel.calculateBalanceAfterCost(privacyCost)
|
|
||||||
val isBalanceSufficient = viewModel.isBalanceSufficient(privacyCost)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(color = Color(0xFFFAFAFB)),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
|
|
||||||
// 顶部导航栏
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(color = Color(0xFFFAFAFB))
|
|
||||||
.padding(horizontal = 14.dp, vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = "返回",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
navController.navigateUp()
|
|
||||||
},
|
|
||||||
colorFilter = ColorFilter.tint(appColors.text)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(12.dp))
|
|
||||||
Text(
|
|
||||||
"编辑Ai",
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
fontSize = 17.sp,
|
|
||||||
color = appColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(1.dp))
|
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
.background(Color(0xFFFAFAFB))
|
|
||||||
) {
|
|
||||||
// 头像选择
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 18.dp),
|
|
||||||
horizontalAlignment = 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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
viewModel.isSelectingAvatar = true
|
|
||||||
// 标记为编辑模式
|
|
||||||
AddAgentViewModel.isSelectingAvatar = false
|
|
||||||
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
viewModel.croppedBitmap != null -> {
|
|
||||||
Image(
|
|
||||||
bitmap = viewModel.croppedBitmap!!.asImageBitmap(),
|
|
||||||
contentDescription = "Avatar",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(72.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
viewModel.avatarUrl != null -> {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = viewModel.avatarUrl!!,
|
|
||||||
contentDescription = "Avatar",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(72.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
|
||||||
contentDescription = "Edit",
|
|
||||||
colorFilter = ColorFilter.tint(Color.White),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
|
||||||
|
|
||||||
// 名称输入
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.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(
|
|
||||||
value = viewModel.title,
|
|
||||||
hint = stringResource(R.string.agent_name_hint_1),
|
|
||||||
background = Color.White,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) { value ->
|
|
||||||
viewModel.title = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
|
||||||
|
|
||||||
// 描述输入
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 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(
|
|
||||||
value = viewModel.desc,
|
|
||||||
hint = stringResource(R.string.agent_desc_hint),
|
|
||||||
background = Color.White,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) { value ->
|
|
||||||
viewModel.desc = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(18.dp))
|
|
||||||
|
|
||||||
// 设定权限区域
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "设定权限",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = appColors.text,
|
|
||||||
fontWeight = FontWeight.W600
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// 公开/私有切换
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(25.dp))
|
|
||||||
.background(Color.White)
|
|
||||||
.border(
|
|
||||||
width = 1.dp,
|
|
||||||
color = Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
|
||||||
shape = RoundedCornerShape(25.dp)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (viewModel.isPublic) "公开" else "私有",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = appColors.text,
|
|
||||||
fontWeight = FontWeight.W500
|
|
||||||
)
|
|
||||||
Switch(
|
|
||||||
checked = viewModel.isPublic,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
if (!checked && needsPayment && !viewModel.paidForPrivacyEdit) {
|
|
||||||
// 需要付费,显示确认对话框
|
|
||||||
showPrivacyConfirmDialog = true
|
|
||||||
} else {
|
|
||||||
viewModel.isPublic = checked
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = SwitchDefaults.colors(
|
|
||||||
checkedThumbColor = Color.White,
|
|
||||||
checkedTrackColor = appColors.brandColorsColor,
|
|
||||||
uncheckedThumbColor = Color.White,
|
|
||||||
uncheckedTrackColor = appColors.brandColorsColor.copy(alpha = 0.5f),
|
|
||||||
uncheckedBorderColor = Color.Transparent
|
|
||||||
),
|
|
||||||
modifier = Modifier.size(width = 64.dp, height = 28.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 首次解锁AI权限提示
|
|
||||||
if (needsPayment && !viewModel.paidForPrivacyEdit && privacyCost > 0) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
// 主要内容容器(去掉阴影)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(
|
|
||||||
color = Color(red = 251f / 255f, green = 248f / 255f, blue = 239f / 255f)
|
|
||||||
)
|
|
||||||
.border(
|
|
||||||
width = 1.dp,
|
|
||||||
color = Color(red = 243f / 255f, green = 234f / 255f, blue = 206f / 255f),
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
)
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
// 锁图标容器
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.background(
|
|
||||||
color = Color(red = 1f, green = 204f / 255f, blue = 0f, alpha = 0.12f),
|
|
||||||
shape = RoundedCornerShape(10.7.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// 锁图标(使用文本代替,实际项目中可以使用图片资源)
|
|
||||||
Text(
|
|
||||||
text = "🔒",
|
|
||||||
fontSize = 18.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "首次解锁Ai权限",
|
|
||||||
fontSize = 13.sp,
|
|
||||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f),
|
|
||||||
fontWeight = FontWeight.W500
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "将消耗",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "$privacyCost",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color(red = 1f, green = 141f / 255f, blue = 40f / 255f)
|
|
||||||
)
|
|
||||||
// 小硬币图标
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(16.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFFFFD700),
|
|
||||||
Color(0xFFFFA500)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = CircleShape
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "解锁后可随时切换",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部保存按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(color = Color(0xFFFAFAFB))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
ActionButton(
|
|
||||||
text = "保存",
|
|
||||||
enabled = !viewModel.isUpdating && !viewModel.isLoading,
|
|
||||||
isLoading = viewModel.isUpdating,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
// 验证输入
|
|
||||||
val validationError = viewModel.validate()
|
|
||||||
if (validationError != null) {
|
|
||||||
errorMessage = validationError
|
|
||||||
return@ActionButton
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要付费确认
|
|
||||||
if (needsPayment && !viewModel.paidForPrivacyEdit) {
|
|
||||||
showPrivacyConfirmDialog = true
|
|
||||||
return@ActionButton
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行保存
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
viewModel.updatePrompt(context)
|
|
||||||
navController.navigateUp()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = e.message ?: "保存失败"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐私权限付费确认对话框
|
|
||||||
if (showPrivacyConfirmDialog) {
|
|
||||||
PointsPaymentDialog(
|
|
||||||
cost = privacyCost,
|
|
||||||
currentBalance = currentBalance,
|
|
||||||
balanceAfterCost = balanceAfterCost,
|
|
||||||
isBalanceSufficient = isBalanceSufficient,
|
|
||||||
onConfirm = {
|
|
||||||
showPrivacyConfirmDialog = false
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
viewModel.isPublic = false
|
|
||||||
viewModel.updatePrompt(context)
|
|
||||||
viewModel.paidForPrivacyEdit = true
|
|
||||||
navController.navigateUp()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = e.message ?: "保存失败"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
showPrivacyConfirmDialog = false
|
|
||||||
},
|
|
||||||
title = "首次解锁AI权限",
|
|
||||||
description = "将消耗 $privacyCost 派币解锁后可随时切换"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误提示
|
|
||||||
errorMessage?.let { error ->
|
|
||||||
LaunchedEffect(error) {
|
|
||||||
kotlinx.coroutines.delay(3000)
|
|
||||||
errorMessage = null
|
|
||||||
}
|
|
||||||
// TODO: 显示Toast或Snackbar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.agent
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.aiosman.ravenow.data.UploadImage
|
|
||||||
import com.aiosman.ravenow.data.ServiceException
|
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
|
||||||
import com.aiosman.ravenow.entity.AgentEntity
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class AiPromptEditViewModel : ViewModel() {
|
|
||||||
var chatAIId by mutableStateOf("")
|
|
||||||
var title by mutableStateOf("")
|
|
||||||
var desc by mutableStateOf("")
|
|
||||||
var isPublic by mutableStateOf(true)
|
|
||||||
var originalIsPublic by mutableStateOf(true)
|
|
||||||
var paidForPrivacyEdit by mutableStateOf(false)
|
|
||||||
var avatarUrl by mutableStateOf<String?>(null)
|
|
||||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
|
||||||
var isUpdating by mutableStateOf(false)
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
|
||||||
var isSelectingAvatar by mutableStateOf(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载Prompt详情
|
|
||||||
*/
|
|
||||||
fun loadPromptDetail(chatAIId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
isLoading = true
|
|
||||||
errorMessage = null
|
|
||||||
this@AiPromptEditViewModel.chatAIId = chatAIId
|
|
||||||
|
|
||||||
val response = ApiClient.api.getPromptDetail(chatAIId)
|
|
||||||
val body = response.body()?.data ?: throw ServiceException("Failed to get prompt detail")
|
|
||||||
|
|
||||||
// 填充数据
|
|
||||||
title = body.title
|
|
||||||
desc = body.desc
|
|
||||||
isPublic = body.isPublic
|
|
||||||
originalIsPublic = body.isPublic
|
|
||||||
avatarUrl = "${ApiClient.BASE_API_URL}/outside${body.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
|
|
||||||
|
|
||||||
// 注意:Agent数据模型可能没有paidForPrivacyEdit字段,需要从其他地方获取
|
|
||||||
// 暂时设为false,后续可以根据实际API响应调整
|
|
||||||
paidForPrivacyEdit = false
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AiPromptEditViewModel", "Error loading prompt detail", e)
|
|
||||||
errorMessage = "加载失败: ${e.message}"
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新Prompt
|
|
||||||
*/
|
|
||||||
suspend fun updatePrompt(context: Context): AgentEntity? {
|
|
||||||
try {
|
|
||||||
isUpdating = true
|
|
||||||
errorMessage = null
|
|
||||||
|
|
||||||
// 准备头像文件
|
|
||||||
val avatarFile = if (croppedBitmap != null) {
|
|
||||||
val file = File(context.cacheDir, "agent_avatar_edit.jpg")
|
|
||||||
croppedBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
|
||||||
UploadImage(file, "agent_avatar_edit.jpg", "", "jpg")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备请求参数
|
|
||||||
val textTitle = title.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
|
||||||
val textDesc = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
|
||||||
val textValue = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull()) // value通常和desc相同
|
|
||||||
val isPublicBody = isPublic.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
|
||||||
|
|
||||||
val avatarPart: MultipartBody.Part? = avatarFile?.let {
|
|
||||||
val requestFile = it.file.asRequestBody("image/*".toMediaTypeOrNull())
|
|
||||||
MultipartBody.Part.createFormData("avatar", it.filename, requestFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用更新API
|
|
||||||
val response = ApiClient.api.updatePrompt(
|
|
||||||
promptId = chatAIId,
|
|
||||||
avatar = avatarPart,
|
|
||||||
title = textTitle,
|
|
||||||
desc = textDesc,
|
|
||||||
value = textValue,
|
|
||||||
isPublic = isPublicBody
|
|
||||||
)
|
|
||||||
|
|
||||||
val body = response.body()?.data ?: throw ServiceException("Failed to update prompt")
|
|
||||||
|
|
||||||
// 更新本地状态
|
|
||||||
originalIsPublic = isPublic
|
|
||||||
|
|
||||||
return body.toAgentEntity()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("AiPromptEditViewModel", "Error updating prompt", e)
|
|
||||||
errorMessage = "更新失败: ${e.message}"
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
isUpdating = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证输入
|
|
||||||
*/
|
|
||||||
fun validate(): String? {
|
|
||||||
return when {
|
|
||||||
title.trim().isEmpty() -> "智能体名称不能为空"
|
|
||||||
title.trim().length < 2 -> "智能体名称长度不能少于2个字符"
|
|
||||||
title.trim().length > 20 -> "智能体名称长度不能超过20个字符"
|
|
||||||
desc.trim().isEmpty() -> "智能体描述不能为空"
|
|
||||||
desc.trim().length > 512 -> "智能体描述长度不能超过512个字符"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否需要付费解锁隐私切换
|
|
||||||
*/
|
|
||||||
fun needsPrivacyPayment(): Boolean {
|
|
||||||
// 如果已经解锁过,则不需要付费
|
|
||||||
if (paidForPrivacyEdit) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 只有从公开(true)切换到私有(false)才需要付费
|
|
||||||
return originalIsPublic == true && isPublic == false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取解锁隐私权限的费用
|
|
||||||
* @return 费用金额,如果无法获取则返回 0
|
|
||||||
*/
|
|
||||||
fun getPrivacyCost(): Int {
|
|
||||||
val rules = PointService.pointsRules.value
|
|
||||||
val costRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_AGENT_PRIVATE)
|
|
||||||
return when (costRule) {
|
|
||||||
is PointService.RuleAmount.Fixed -> costRule.value
|
|
||||||
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
|
|
||||||
null -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前余额
|
|
||||||
* @return 当前余额,如果无法获取则返回 0
|
|
||||||
*/
|
|
||||||
fun getCurrentBalance(): Int {
|
|
||||||
return PointService.pointsBalance.value?.balance ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算消耗后余额
|
|
||||||
* @param cost 费用
|
|
||||||
* @return 消耗后余额
|
|
||||||
*/
|
|
||||||
fun calculateBalanceAfterCost(cost: Int): Int {
|
|
||||||
val currentBalance = getCurrentBalance()
|
|
||||||
return (currentBalance - cost).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查余额是否充足
|
|
||||||
* @param cost 费用
|
|
||||||
* @return 是否充足
|
|
||||||
*/
|
|
||||||
fun isBalanceSufficient(cost: Int): Boolean {
|
|
||||||
return getCurrentBalance() >= cost
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空数据
|
|
||||||
*/
|
|
||||||
fun clearData() {
|
|
||||||
chatAIId = ""
|
|
||||||
title = ""
|
|
||||||
desc = ""
|
|
||||||
isPublic = true
|
|
||||||
originalIsPublic = true
|
|
||||||
paidForPrivacyEdit = false
|
|
||||||
avatarUrl = null
|
|
||||||
croppedBitmap = null
|
|
||||||
isUpdating = false
|
|
||||||
isLoading = false
|
|
||||||
errorMessage = null
|
|
||||||
isSelectingAvatar = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -617,7 +617,7 @@ fun ChatAiInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ fun ChatInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -183,21 +183,45 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
|
if (viewModel.groupAvatar.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = viewModel.groupInfo!!.groupAvatar,
|
imageUrl = viewModel.groupAvatar,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(35.dp)
|
.size(32.dp)
|
||||||
.clip(RoundedCornerShape(15.dp)),
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
contentDescription = "群聊头像"
|
contentDescription = "群聊头像"
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(AppColors.decentBackground)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToGroupInfo(groupId)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.groupName,
|
||||||
|
style = TextStyle(
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow =TextOverflow.Ellipsis,
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Column(
|
Column {
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.Start
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.groupName,
|
text = viewModel.groupName,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
@@ -206,10 +230,12 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||||
),
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow =TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -217,11 +243,12 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToGroupInfo(groupId)
|
navController.navigateToGroupInfo(groupId)
|
||||||
},
|
},
|
||||||
contentDescription = "更多",
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Column(
|
Column(
|
||||||
@@ -650,7 +677,7 @@ fun GroupChatInput(
|
|||||||
animationSpec = tween(300)
|
animationSpec = tween(300)
|
||||||
)
|
)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.btn),
|
painter = painterResource(R.mipmap.rider_pro_im_send),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.alpha(alpha)
|
.alpha(alpha)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.aiosman.ravenow.ui.chat
|
package com.aiosman.ravenow.ui.chat
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
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
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
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.SendChatAiRequestBody
|
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
|
||||||
import io.openim.android.sdk.enums.ConversationType
|
import io.openim.android.sdk.enums.ConversationType
|
||||||
@@ -52,34 +50,7 @@ class GroupChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getGroupInfo() {
|
private suspend fun getGroupInfo() {
|
||||||
try {
|
// 简化群组信息获取,使用默认信息
|
||||||
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
|
|
||||||
val room = response.body()?.data
|
|
||||||
groupInfo = room?.let {
|
|
||||||
GroupInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
groupName = it.name,
|
|
||||||
groupAvatar = if (it.avatar.isNullOrEmpty()) {
|
|
||||||
val groupIdBase64 = Base64.encodeToString(
|
|
||||||
groupId.toByteArray(),
|
|
||||||
Base64.NO_WRAP
|
|
||||||
)
|
|
||||||
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
|
|
||||||
} else {
|
|
||||||
"${ApiClient.BASE_API_URL}/outside${it.avatar}?token=${AppStore.token}"
|
|
||||||
},
|
|
||||||
memberCount = it.userCount,
|
|
||||||
ownerId = it.creator.userId
|
|
||||||
)
|
|
||||||
} ?: GroupInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
groupName = name,
|
|
||||||
groupAvatar = avatar,
|
|
||||||
memberCount = 0,
|
|
||||||
ownerId = ""
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("GroupChatViewModel", "加载群信息失败: ${e.message}", e)
|
|
||||||
groupInfo = GroupInfo(
|
groupInfo = GroupInfo(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
groupName = name,
|
groupName = name,
|
||||||
@@ -87,12 +58,10 @@ class GroupChatViewModel(
|
|||||||
memberCount = 0,
|
memberCount = 0,
|
||||||
ownerId = ""
|
ownerId = ""
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
groupName = groupInfo?.groupName ?: ""
|
groupName = groupInfo?.groupName ?: ""
|
||||||
groupAvatar = groupInfo?.groupAvatar ?: ""
|
groupAvatar = groupInfo?.groupAvatar ?: ""
|
||||||
memberCount = groupInfo?.memberCount ?: 0
|
memberCount = groupInfo?.memberCount ?: 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
override fun getConversationParams(): Triple<String, Int, Boolean> {
|
||||||
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
// 根据群组类型决定ConversationType,这里假设是普通群聊
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ 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.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
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.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -30,11 +26,9 @@ 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
|
||||||
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.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@@ -42,12 +36,10 @@ 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
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
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.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||||
@@ -84,9 +76,8 @@ class CommentModalViewModel(
|
|||||||
fun CommentModalContent(
|
fun CommentModalContent(
|
||||||
postId: Int? = null,
|
postId: Int? = null,
|
||||||
commentCount: Int = 0,
|
commentCount: Int = 0,
|
||||||
onDismiss: () -> Unit = {},
|
onCommentAdded: () -> Unit = {},
|
||||||
showTitle: Boolean = true,
|
onDismiss: () -> Unit = {}
|
||||||
onCommentAdded: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
@@ -97,7 +88,6 @@ fun CommentModalContent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
val commentViewModel = model.commentsViewModel
|
val commentViewModel = model.commentsViewModel
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
||||||
@@ -109,26 +99,10 @@ fun CommentModalContent(
|
|||||||
var bottomPadding by remember { mutableStateOf(0.dp) }
|
var bottomPadding by remember { mutableStateOf(0.dp) }
|
||||||
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||||
var shouldAutoFocus by remember { mutableStateOf(false) }
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(imePadding) {
|
LaunchedEffect(imePadding) {
|
||||||
bottomPadding = imePadding.dp
|
bottomPadding = imePadding.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当设置回复评论时,自动聚焦到输入框
|
|
||||||
LaunchedEffect(replyComment) {
|
|
||||||
if (replyComment != null) {
|
|
||||||
// 延迟一下,确保输入框已经渲染
|
|
||||||
kotlinx.coroutines.delay(100)
|
|
||||||
shouldAutoFocus = true
|
|
||||||
// 请求显示键盘
|
|
||||||
softwareKeyboardController?.show()
|
|
||||||
} else {
|
|
||||||
shouldAutoFocus = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
@@ -139,12 +113,13 @@ fun CommentModalContent(
|
|||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
},
|
},
|
||||||
containerColor = AppColors.background,
|
containerColor = Color.White,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
dragHandle = {},
|
dragHandle = {},
|
||||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
|
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||||
|
windowInsets = WindowInsets(0)
|
||||||
) {
|
) {
|
||||||
CommentMenuModal(
|
CommentMenuModal(
|
||||||
onDeleteClick = {
|
onDeleteClick = {
|
||||||
@@ -167,25 +142,7 @@ fun CommentModalContent(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
) {
|
||||||
// 拖动手柄
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp, bottom = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(40.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
.clip(RoundedCornerShape(50))
|
|
||||||
.background(AppColors.divider)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showTitle) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -196,43 +153,39 @@ fun CommentModalContent(
|
|||||||
stringResource(R.string.comment),
|
stringResource(R.string.comment),
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = AppColors.text,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
HorizontalDivider(
|
||||||
|
color = Color(0xFFF7F7F7)
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 20.dp, vertical = 8.dp),
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.comment_count, commentCount),
|
text = stringResource(id = R.string.comment_count, commentCount),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = AppColors.secondaryText
|
color = Color(0xff666666)
|
||||||
)
|
)
|
||||||
OrderSelectionComponent(
|
OrderSelectionComponent {
|
||||||
selectedOrder = commentViewModel.order
|
|
||||||
) {
|
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
commentViewModel.reloadComment()
|
commentViewModel.reloadComment()
|
||||||
scope.launch {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
state = listState
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
CommentContent(
|
CommentContent(
|
||||||
@@ -241,9 +194,7 @@ fun CommentModalContent(
|
|||||||
|
|
||||||
},
|
},
|
||||||
onReply = { parentComment, _, _, _ ->
|
onReply = { parentComment, _, _, _ ->
|
||||||
// 设置回复的评论,这样 EditCommentBottomModal 会显示回复输入框
|
|
||||||
// CommentContent 内部已经处理了游客模式检查,所以这里直接设置即可
|
|
||||||
replyComment = parentComment
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(72.dp))
|
Spacer(modifier = Modifier.height(72.dp))
|
||||||
@@ -254,12 +205,9 @@ fun CommentModalContent(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(AppColors.secondaryBackground)
|
.background(Color(0xfff7f7f7))
|
||||||
) {
|
|
||||||
EditCommentBottomModal(
|
|
||||||
replyComment = replyComment,
|
|
||||||
autoFocus = shouldAutoFocus
|
|
||||||
) {
|
) {
|
||||||
|
EditCommentBottomModal(replyComment) {
|
||||||
commentViewModel.viewModelScope.launch {
|
commentViewModel.viewModelScope.launch {
|
||||||
if (replyComment != null) {
|
if (replyComment != null) {
|
||||||
if (replyComment?.parentCommentId != null) {
|
if (replyComment?.parentCommentId != null) {
|
||||||
@@ -277,13 +225,6 @@ fun CommentModalContent(
|
|||||||
// 顶级评论
|
// 顶级评论
|
||||||
commentViewModel.createComment(it)
|
commentViewModel.createComment(it)
|
||||||
}
|
}
|
||||||
// 评论创建成功后调用回调
|
|
||||||
onCommentAdded()
|
|
||||||
// 清空回复状态和自动聚焦状态
|
|
||||||
replyComment = null
|
|
||||||
shouldAutoFocus = false
|
|
||||||
// 隐藏键盘
|
|
||||||
softwareKeyboardController?.hide()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,16 @@ import com.aiosman.ravenow.R
|
|||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
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.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.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.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
fun CommentNoticeScreen() {
|
||||||
val viewModel = viewModel<CommentNoticeListViewModel>(
|
val viewModel = viewModel<CommentNoticeListViewModel>(
|
||||||
key = "CommentNotice",
|
key = "CommentNotice",
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -67,22 +68,50 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().background(color = AppColors.background)
|
||||||
.background(color = AppColors.background)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
darkIcons = !AppState.darkMode,
|
|
||||||
maskBoxBackgroundColor = AppColors.background,
|
|
||||||
includeStatusBarPadding = includeStatusBarPadding
|
|
||||||
) {
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 149.dp),
|
||||||
|
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)
|
viewModel.initData(context, force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -95,25 +124,33 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
androidx.compose.foundation.Image(
|
androidx.compose.foundation.Image(
|
||||||
painter = painterResource(id = R.mipmap.invalid_name_5),
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.tietie_dark
|
||||||
|
else R.mipmap.invalid_name_11),
|
||||||
contentDescription = "No Comment",
|
contentDescription = "No Comment",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 181.dp, height = 153.dp)
|
.size(width = 181.dp, height = 153.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(9.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.no_one_pinged_yet),
|
text = "等一位旅人~",
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "去发布动态,让更多人参与对话",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.fillMaxSize().padding(horizontal = 16.dp)
|
||||||
.background(color = AppColors.background)
|
|
||||||
) {
|
) {
|
||||||
items(comments.itemCount) { index ->
|
items(comments.itemCount) { index ->
|
||||||
comments[index]?.let { comment ->
|
comments[index]?.let { comment ->
|
||||||
@@ -187,18 +224,17 @@ fun CommentNoticeItem(
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val commentPrefix = stringResource(R.string.comment_notice)
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
// 左侧头像区域
|
Box {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context = context,
|
||||||
imageUrl = commentItem.avatar,
|
imageUrl = commentItem.avatar,
|
||||||
contentDescription = commentItem.name,
|
contentDescription = commentItem.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
@@ -209,36 +245,32 @@ fun CommentNoticeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
// 右侧内容区域
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 8.dp)
|
.padding(start = 12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
onPostClick()
|
onPostClick()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// 主要信息列
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = commentItem.name,
|
text = commentItem.name,
|
||||||
fontSize = 14.sp,
|
fontSize = 18.sp,
|
||||||
color = AppColors.text,
|
modifier = Modifier,
|
||||||
maxLines = 1,
|
color = AppColors.text
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
// 评论内容行
|
|
||||||
Row {
|
Row {
|
||||||
var text = commentItem.comment
|
var text = commentItem.comment
|
||||||
if (commentItem.parentCommentId != null) {
|
if (commentItem.parentCommentId != null) {
|
||||||
text = "Reply you: $text"
|
text = "Reply you: $text"
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "$commentPrefix $text",
|
text = text,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
@@ -252,20 +284,25 @@ fun CommentNoticeItem(
|
|||||||
color = AppColors.secondaryText,
|
color = AppColors.secondaryText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
// 右侧帖子图片
|
|
||||||
commentItem.post?.let {
|
commentItem.post?.let {
|
||||||
Box {
|
Box {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
context = context,
|
context = context,
|
||||||
imageUrl = it.images[0].thumbnail,
|
imageUrl = it.images[0].thumbnail,
|
||||||
contentDescription = "Post Image",
|
contentDescription = "Post Image",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(48.dp).clip(RoundedCornerShape(8.dp))
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
)
|
)
|
||||||
// 未读指示器
|
// unread indicator
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (commentItem.unread) {
|
if (commentItem.unread) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -275,7 +312,11 @@ fun CommentNoticeItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,7 @@ fun AgentCard(
|
|||||||
agentEntity.avatar,
|
agentEntity.avatar,
|
||||||
contentDescription = agentEntity.openId,
|
contentDescription = agentEntity.openId,
|
||||||
modifier = Modifier.size(40.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop
|
||||||
defaultRes = com.aiosman.ravenow.R.mipmap.group_copy
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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
|
||||||
|
|
||||||
@@ -37,13 +36,6 @@ fun AnimatedCounter(count: Int, modifier: Modifier = Modifier, fontSize: Int = 2
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { targetCount ->
|
) { targetCount ->
|
||||||
Text(
|
Text(text = "$targetCount", modifier = modifier, fontSize = fontSize.sp, color = AppColors.text)
|
||||||
text = "$targetCount",
|
|
||||||
modifier = modifier,
|
|
||||||
fontSize = fontSize.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,6 @@ fun AnimatedFavouriteIcon(
|
|||||||
modifier = modifier.graphicsLayer {
|
modifier = modifier.graphicsLayer {
|
||||||
rotationZ = animatableRotation.value
|
rotationZ = animatableRotation.value
|
||||||
},
|
},
|
||||||
colorFilter = if (!isFavourite) ColorFilter.tint(AppColors.text) else null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,9 @@ import androidx.compose.ui.Modifier
|
|||||||
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.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil3.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
import coil3.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil3.request.crossfade
|
|
||||||
import coil3.request.fallback
|
|
||||||
import coil3.request.placeholder
|
|
||||||
import com.aiosman.ravenow.utils.BlurHashDecoder
|
import com.aiosman.ravenow.utils.BlurHashDecoder
|
||||||
import com.aiosman.ravenow.utils.Utils.getImageLoader
|
import com.aiosman.ravenow.utils.Utils.getImageLoader
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.composables
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
@@ -69,6 +68,7 @@ fun Modifier.debouncedClickableWithRipple(
|
|||||||
clickable(
|
clickable(
|
||||||
enabled = enabled && isClickable,
|
enabled = enabled && isClickable,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = androidx.compose.material.ripple.rememberRipple()
|
||||||
) {
|
) {
|
||||||
if (isClickable) {
|
if (isClickable) {
|
||||||
isClickable = false
|
isClickable = false
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ fun LazyGridItemScope.DraggableItem(
|
|||||||
translationY = dragDropState.previousItemOffset.value.y
|
translationY = dragDropState.previousItemOffset.value.y
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier.animateItemPlacement()
|
||||||
}
|
}
|
||||||
Box(modifier = modifier.then(draggingModifier).clip(RoundedCornerShape(8.dp)), propagateMinConstraints = true) {
|
Box(modifier = modifier.then(draggingModifier).clip(RoundedCornerShape(8.dp)), propagateMinConstraints = true) {
|
||||||
content(dragging)
|
content(dragging)
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ 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.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -61,15 +59,10 @@ fun EditCommentBottomModal(
|
|||||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
LaunchedEffect(autoFocus) {
|
LaunchedEffect(autoFocus) {
|
||||||
if (autoFocus) {
|
if (autoFocus) {
|
||||||
// 延迟一下,确保输入框已经渲染完成
|
|
||||||
kotlinx.coroutines.delay(150)
|
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
// 显示键盘
|
|
||||||
keyboardController?.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +82,7 @@ fun EditCommentBottomModal(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(AppColors.inputBackground)
|
.background(Color.Gray.copy(alpha = 0.1f))
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -106,7 +99,7 @@ fun EditCommentBottomModal(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = AppColors.text,
|
color = Color.Black,
|
||||||
fontWeight = FontWeight.Normal
|
fontWeight = FontWeight.Normal
|
||||||
),
|
),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
@@ -117,11 +110,7 @@ fun EditCommentBottomModal(
|
|||||||
innerTextField()
|
innerTextField()
|
||||||
if (text.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = if (replyComment == null) {
|
text = if (replyComment == null) "快来互动吧..." else "回复@${replyComment.name}",
|
||||||
stringResource(R.string.post_comment_hint)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.reply_to_user, replyComment.name ?: "")
|
|
||||||
},
|
|
||||||
color = AppColors.text.copy(alpha = 0.3f), // 30%透明度
|
color = AppColors.text.copy(alpha = 0.3f), // 30%透明度
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
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.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import coil3.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil3.asDrawable
|
import coil.compose.AsyncImage
|
||||||
import coil3.asImage
|
import coil.request.ImageRequest
|
||||||
import coil3.compose.AsyncImage
|
import coil.request.SuccessResult
|
||||||
import coil3.request.CachePolicy
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import coil3.request.SuccessResult
|
|
||||||
import coil3.request.crossfade
|
|
||||||
import com.aiosman.ravenow.utils.Utils.getImageLoader
|
import com.aiosman.ravenow.utils.Utils.getImageLoader
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -64,11 +59,7 @@ fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
val successResult = imageLoader.execute(request) as? SuccessResult
|
(imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap()
|
||||||
successResult?.let {
|
|
||||||
val drawable = it.image.asDrawable(context.resources)
|
|
||||||
drawable.toBitmap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap = result
|
bitmap = result
|
||||||
@@ -147,33 +138,25 @@ fun CustomAsyncImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理字符串URL
|
// 处理字符串URL
|
||||||
val ctx = context ?: localContext
|
|
||||||
val placeholderImage = remember(placeholderRes, ctx) {
|
|
||||||
placeholderRes?.let { resId ->
|
|
||||||
ContextCompat.getDrawable(ctx, resId)?.asImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val errorImage = remember(errorRes, ctx) {
|
|
||||||
errorRes?.let { resId ->
|
|
||||||
ContextCompat.getDrawable(ctx, resId)?.asImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showShimmer) {
|
if (showShimmer) {
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(ctx)
|
model = ImageRequest.Builder(context ?: localContext)
|
||||||
.data(imageUrl)
|
.data(imageUrl)
|
||||||
.crossfade(200)
|
.crossfade(200)
|
||||||
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
|
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
|
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.apply {
|
.apply {
|
||||||
// 设置占位符图片
|
// 设置占位符图片
|
||||||
placeholderImage?.let { placeholder(it) }
|
if (placeholderRes != null) {
|
||||||
|
placeholder(placeholderRes)
|
||||||
|
}
|
||||||
// 设置错误时显示的图片
|
// 设置错误时显示的图片
|
||||||
errorImage?.let { error(it) }
|
if (errorRes != null) {
|
||||||
|
error(errorRes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
@@ -194,16 +177,20 @@ fun CustomAsyncImage(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(ctx)
|
model = ImageRequest.Builder(context ?: localContext)
|
||||||
.data(imageUrl)
|
.data(imageUrl)
|
||||||
.crossfade(200)
|
.crossfade(200)
|
||||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.diskCachePolicy(CachePolicy.ENABLED)
|
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.apply {
|
.apply {
|
||||||
// 设置占位符图片
|
// 设置占位符图片
|
||||||
placeholderImage?.let { placeholder(it) }
|
if (placeholderRes != null) {
|
||||||
|
placeholder(placeholderRes)
|
||||||
|
}
|
||||||
// 设置错误时显示的图片
|
// 设置错误时显示的图片
|
||||||
errorImage?.let { error(it) }
|
if (errorRes != null) {
|
||||||
|
error(errorRes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ fun MomentCard(
|
|||||||
) {
|
) {
|
||||||
MomentContentGroup(
|
MomentContentGroup(
|
||||||
momentEntity = momentEntity,
|
momentEntity = momentEntity,
|
||||||
imageIndex = imageIndex,
|
|
||||||
onPageChange = { index -> imageIndex = index }
|
onPageChange = { index -> imageIndex = index }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,6 +120,7 @@ fun MomentCard(
|
|||||||
onLikeClick = onLikeClick,
|
onLikeClick = onLikeClick,
|
||||||
onAddComment = onAddComment,
|
onAddComment = onAddComment,
|
||||||
onFavoriteClick = onFavoriteClick,
|
onFavoriteClick = onFavoriteClick,
|
||||||
|
imageIndex = imageIndex,
|
||||||
onCommentClick = {
|
onCommentClick = {
|
||||||
navController.navigateToPost(
|
navController.navigateToPost(
|
||||||
momentEntity.id,
|
momentEntity.id,
|
||||||
@@ -327,11 +327,6 @@ fun PostImageView(
|
|||||||
images: List<MomentImageEntity>,
|
images: List<MomentImageEntity>,
|
||||||
onPageChange: (Int) -> Unit = {}
|
onPageChange: (Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
// 如果图片列表为空,不渲染任何内容
|
|
||||||
if (images.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val pagerState = rememberPagerState(pageCount = { images.size })
|
val pagerState = rememberPagerState(pageCount = { images.size })
|
||||||
LaunchedEffect(pagerState.currentPage) {
|
LaunchedEffect(pagerState.currentPage) {
|
||||||
onPageChange(pagerState.currentPage)
|
onPageChange(pagerState.currentPage)
|
||||||
@@ -366,88 +361,27 @@ fun PostImageView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MomentContentGroup(
|
fun MomentContentGroup(
|
||||||
momentEntity: MomentEntity,
|
momentEntity: MomentEntity,
|
||||||
imageIndex: Int = 0,
|
|
||||||
onPageChange: (Int) -> Unit = {}
|
onPageChange: (Int) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
if (momentEntity.relMoment != null) {
|
if (momentEntity.relMoment != null) {
|
||||||
RelPostCard(
|
RelPostCard(
|
||||||
momentEntity = momentEntity.relMoment!!,
|
momentEntity = momentEntity.relMoment!!,
|
||||||
modifier = Modifier.background(Color(0xFFF8F8F8))
|
modifier = Modifier.background(Color(0xFFF8F8F8))
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
// 优先显示图片,如果没有图片则显示视频缩略图
|
|
||||||
if (momentEntity.images.isNotEmpty()) {
|
|
||||||
PostImageView(
|
PostImageView(
|
||||||
images = momentEntity.images,
|
images = momentEntity.images,
|
||||||
onPageChange = onPageChange
|
onPageChange = onPageChange
|
||||||
)
|
)
|
||||||
} else if (momentEntity.videos != null && momentEntity.videos.isNotEmpty()) {
|
|
||||||
// 显示视频缩略图
|
|
||||||
val firstVideo = momentEntity.videos.first()
|
|
||||||
val thumbnailUrl = firstVideo.thumbnailUrl ?: firstVideo.thumbnailDirectUrl
|
|
||||||
if (thumbnailUrl != null) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(
|
|
||||||
if (firstVideo.width != null && firstVideo.height != null && firstVideo.height > 0) {
|
|
||||||
firstVideo.width.toFloat() / firstVideo.height.toFloat()
|
|
||||||
} else {
|
|
||||||
1f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = thumbnailUrl,
|
|
||||||
contentDescription = "Video thumbnail",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (momentEntity.momentTextContent.isNotEmpty()) {
|
||||||
}
|
|
||||||
|
|
||||||
// 图片指示器:显示在图片下方、文案上方
|
|
||||||
if (momentEntity.images.size > 1) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 8.dp),
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!momentEntity.momentTextContent.isNullOrEmpty()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = com.aiosman.ravenow.utils.Utils.unescapeHtml(momentEntity.momentTextContent),
|
text = momentEntity.momentTextContent,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||||
@@ -468,7 +402,6 @@ 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)
|
|
||||||
)
|
)
|
||||||
if (count.isNotEmpty()) {
|
if (count.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
@@ -493,6 +426,7 @@ 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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +438,8 @@ fun MomentBottomOperateRowGroup(
|
|||||||
onAddComment: () -> Unit = {},
|
onAddComment: () -> Unit = {},
|
||||||
onCommentClick: () -> Unit = {},
|
onCommentClick: () -> Unit = {},
|
||||||
onFavoriteClick: () -> Unit = {},
|
onFavoriteClick: () -> Unit = {},
|
||||||
momentEntity: MomentEntity
|
momentEntity: MomentEntity,
|
||||||
|
imageIndex: Int = 0
|
||||||
) {
|
) {
|
||||||
val lastClickTime = remember { mutableStateOf(0L) }
|
val lastClickTime = remember { mutableStateOf(0L) }
|
||||||
val clickDelay = 500L
|
val clickDelay = 500L
|
||||||
@@ -516,6 +451,7 @@ fun MomentBottomOperateRowGroup(
|
|||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
|
windowInsets = WindowInsets(0),
|
||||||
dragHandle = {
|
dragHandle = {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -540,12 +476,40 @@ fun MomentBottomOperateRowGroup(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.padding(start = 16.dp, end = 16.dp)
|
.padding(start = 16.dp, end = 0.dp)
|
||||||
) {
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (momentEntity.images.size > 1) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(),
|
.weight(1f),
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -561,7 +525,7 @@ fun MomentBottomOperateRowGroup(
|
|||||||
onLikeClick()
|
onLikeClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
// 评论按钮
|
// 评论按钮
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
@@ -577,7 +541,7 @@ fun MomentBottomOperateRowGroup(
|
|||||||
count = momentEntity.commentCount.toString()
|
count = momentEntity.commentCount.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(28.dp))
|
||||||
// 转发按钮
|
// 转发按钮
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
@@ -591,7 +555,6 @@ fun MomentBottomOperateRowGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
// 收藏按钮
|
// 收藏按钮
|
||||||
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
|
MomentOperateBtn(count = momentEntity.favoriteCount.toString()) {
|
||||||
AnimatedFavouriteIcon(
|
AnimatedFavouriteIcon(
|
||||||
@@ -603,6 +566,7 @@ fun MomentBottomOperateRowGroup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,342 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.composables
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.material.icons.filled.Warning
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局付费确认对话框组件
|
|
||||||
* 参考 iOS 版本的 PointsConfirmDialog
|
|
||||||
*
|
|
||||||
* @param cost 需要支付的费用
|
|
||||||
* @param currentBalance 当前余额
|
|
||||||
* @param balanceAfterCost 支付后余额
|
|
||||||
* @param isBalanceSufficient 余额是否充足
|
|
||||||
* @param onConfirm 确认支付回调
|
|
||||||
* @param onCancel 取消回调
|
|
||||||
* @param title 对话框标题
|
|
||||||
* @param description 对话框描述
|
|
||||||
* @param isProcessing 是否正在处理中
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun PointsPaymentDialog(
|
|
||||||
cost: Int,
|
|
||||||
currentBalance: Int,
|
|
||||||
balanceAfterCost: Int,
|
|
||||||
isBalanceSufficient: Boolean,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onCancel: () -> Unit,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
isProcessing: Boolean = false
|
|
||||||
) {
|
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val screenWidth = configuration.screenWidthDp.dp
|
|
||||||
val dialogWidth = (screenWidth - 48.dp).coerceAtMost(360.dp)
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = {
|
|
||||||
if (!isProcessing) {
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
properties = DialogProperties(
|
|
||||||
dismissOnBackPress = !isProcessing,
|
|
||||||
dismissOnClickOutside = !isProcessing
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(dialogWidth)
|
|
||||||
.shadow(
|
|
||||||
elevation = 20.dp,
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
spotColor = Color.Black.copy(alpha = 0.2f)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = appColors.background
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// 顶部图标 - 使用 paip_coin_img
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.mipmap.paip_coin_img),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(80.dp),
|
|
||||||
contentScale = ContentScale.Fit
|
|
||||||
)
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = appColors.text,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
// 描述
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = appColors.secondaryText,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.padding(horizontal = 20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 积分消耗信息区域
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
color = appColors.inputBackground.copy(alpha = 0.5f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
// 需要消耗
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.cost_required),
|
|
||||||
amount = cost,
|
|
||||||
appColors = appColors,
|
|
||||||
amountColor = Color(0xFFFF8C00) // 橙色
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider(color = appColors.divider)
|
|
||||||
|
|
||||||
// 当前余额
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.current_balance),
|
|
||||||
amount = currentBalance,
|
|
||||||
appColors = appColors,
|
|
||||||
amountColor = if (isBalanceSufficient) appColors.text else Color.Red
|
|
||||||
)
|
|
||||||
|
|
||||||
HorizontalDivider(color = appColors.divider)
|
|
||||||
|
|
||||||
// 支付后余额
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.balance_after),
|
|
||||||
amount = balanceAfterCost,
|
|
||||||
appColors = appColors,
|
|
||||||
amountColor = appColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 余额不足提示
|
|
||||||
if (!isBalanceSufficient) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Warning,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Color(0xFFFF8C00), // 橙色
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.insufficient_pai_coin_balance),
|
|
||||||
fontSize = 13.sp,
|
|
||||||
color = Color(0xFFFF8C00), // 橙色
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
// 取消按钮
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (!isProcessing) {
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(50.dp),
|
|
||||||
enabled = !isProcessing,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = appColors.inputBackground,
|
|
||||||
contentColor = appColors.text,
|
|
||||||
disabledContainerColor = appColors.inputBackground,
|
|
||||||
disabledContentColor = appColors.text.copy(alpha = 0.5f)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.cancel),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W500
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(50.dp)
|
|
||||||
.background(
|
|
||||||
brush = if (isBalanceSufficient) {
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
appColors.main,
|
|
||||||
appColors.main
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFFFF8C00), // 橙色
|
|
||||||
Color.Red
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (!isProcessing) {
|
|
||||||
Modifier.noRippleClickable {
|
|
||||||
if (!isBalanceSufficient) {
|
|
||||||
// 积分不足,跳转充值页面
|
|
||||||
onCancel()
|
|
||||||
// 这里可以发送通知或回调来跳转充值页面
|
|
||||||
} else {
|
|
||||||
// 积分充足,确认消费
|
|
||||||
onConfirm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (isProcessing) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
color = Color.White,
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = if (isBalanceSufficient) {
|
|
||||||
stringResource(R.string.confirm_consumption)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.go_recharge)
|
|
||||||
},
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color.White,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用信息行组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun CostInfoRow(
|
|
||||||
label: String,
|
|
||||||
amount: Int,
|
|
||||||
appColors: com.aiosman.ravenow.AppThemeData,
|
|
||||||
amountColor: Color? = null
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = appColors.secondaryText
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
// 星形图标(参考 iOS 版本)
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Star,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp),
|
|
||||||
tint = Color(0xFFFFD700) // 黄色
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "${amount.formatNumber()}",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = amountColor ?: appColors.text
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.pai_coin),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = appColors.secondaryText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化数字,添加千位分隔符
|
|
||||||
*/
|
|
||||||
private fun Int.formatNumber(): String {
|
|
||||||
return this.toString().reversed().chunked(3).joinToString(",").reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@ fun PolicyCheckbox(
|
|||||||
showModal = false
|
showModal = false
|
||||||
},
|
},
|
||||||
sheetState = modalSheetState,
|
sheetState = modalSheetState,
|
||||||
|
windowInsets = WindowInsets(0),
|
||||||
containerColor = Color.White,
|
containerColor = Color.White,
|
||||||
) {
|
) {
|
||||||
WebViewDisplay(
|
WebViewDisplay(
|
||||||
@@ -97,7 +98,7 @@ fun PolicyCheckbox(
|
|||||||
|
|
||||||
addStyle(
|
addStyle(
|
||||||
style = SpanStyle(
|
style = SpanStyle(
|
||||||
color = Color(0xFF7C45ED), // 紫色
|
color = appColor.main,
|
||||||
textDecoration = TextDecoration.Underline
|
textDecoration = TextDecoration.Underline
|
||||||
),
|
),
|
||||||
start = template.length + 1,
|
start = template.length + 1,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ fun StatusBarMaskLayout(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
darkIcons: Boolean = true,
|
darkIcons: Boolean = true,
|
||||||
useNavigationBarMask: Boolean = true,
|
useNavigationBarMask: Boolean = true,
|
||||||
includeStatusBarPadding: Boolean = true,
|
|
||||||
maskBoxBackgroundColor: Color = Color.Transparent,
|
maskBoxBackgroundColor: Color = Color.Transparent,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -51,13 +50,13 @@ fun StatusBarMaskLayout(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (includeStatusBarPadding) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(paddingValues.calculateTopPadding())
|
.height(paddingValues.calculateTopPadding())
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(maskBoxBackgroundColor)
|
.background(maskBoxBackgroundColor)
|
||||||
)
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {
|
if (navigationBarPaddings > 24.dp && useNavigationBarMask) {
|
||||||
|
|||||||
@@ -6,25 +6,18 @@ 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.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.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
|
||||||
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.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.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
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
|
||||||
@@ -46,7 +39,6 @@ fun TabItem(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.wrapContentWidth()
|
|
||||||
.noRippleClickable { onClick() },
|
.noRippleClickable { onClick() },
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
@@ -81,47 +73,30 @@ fun UnderlineTabItem(
|
|||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
// 动画化字体大小和padding
|
Column(
|
||||||
val animatedFontSize by animateFloatAsState(
|
|
||||||
targetValue = if (isSelected) 17f else 15f,
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "fontSize"
|
|
||||||
)
|
|
||||||
val animatedPadding by animateDpAsState(
|
|
||||||
targetValue = if (isSelected) 20.dp else 16.dp,
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "padding"
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.noRippleClickable { onClick() },
|
.noRippleClickable { onClick() },
|
||||||
contentAlignment = Alignment.Center
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// 文本层 - 始终居中,不受下划线影响
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = animatedFontSize.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
|
color = if (isSelected) AppColors.text else AppColors.text.copy(alpha = 0.6f),
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 13.dp)
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = animatedPadding)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 下划线层 - 固定在底部,不影响文本位置
|
// 选中状态下显示图标
|
||||||
if (isSelected) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.size(24.dp),
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.size(24.dp)
|
|
||||||
.offset(y = (15).dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
if (isSelected) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.underline),
|
painter = painterResource(id = R.mipmap.underline),
|
||||||
contentDescription = "selected indicator",
|
contentDescription = "selected indicator",
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
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.LocalAppTheme
|
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
|
||||||
@@ -60,21 +59,17 @@ fun TextInputField(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
customBackgroundColor: Color? = null,
|
customBackgroundColor: Color? = null,
|
||||||
customHintColor: Color? = null,
|
|
||||||
customLabelColor: Color? = null,
|
|
||||||
customCornerRadius: Float = 24f
|
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
|
val backgroundColor = customBackgroundColor ?: AppColors.inputBackground
|
||||||
val hintColor = customHintColor ?: HintTextColor
|
|
||||||
val labelColor = customLabelColor ?: LabelTextColor
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
label?.let {
|
label?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
color = labelColor,
|
color = LabelTextColor,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
|
modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp)
|
||||||
)
|
)
|
||||||
@@ -126,19 +121,13 @@ fun TextInputField(
|
|||||||
if (text.isEmpty() && hint != null) {
|
if (text.isEmpty() && hint != null) {
|
||||||
Text(
|
Text(
|
||||||
text = hint,
|
text = hint,
|
||||||
color = hintColor,
|
color = HintTextColor,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W400
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (password) {
|
if (password) {
|
||||||
// 暗色模式下图标为白色,否则使用默认颜色
|
|
||||||
val iconColor = if (AppState.darkMode) {
|
|
||||||
Color.White
|
|
||||||
} else {
|
|
||||||
PasswordIconColor
|
|
||||||
}
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if (showPassword) {
|
id = if (showPassword) {
|
||||||
@@ -153,7 +142,7 @@ fun TextInputField(
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showPassword = !showPassword
|
showPassword = !showPassword
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(iconColor)
|
colorFilter = ColorFilter.tint(PasswordIconColor)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ fun FormTextInput(
|
|||||||
.let {
|
.let {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
||||||
} else if (background != null && background == Color.White) {
|
|
||||||
// 如果传入白色背景,添加灰色边框
|
|
||||||
it.border(
|
|
||||||
1.dp,
|
|
||||||
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
|
||||||
RoundedCornerShape(25.dp)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,13 +68,6 @@ fun FormTextInput2(
|
|||||||
.let {
|
.let {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
||||||
} else if (background != null && background == Color.White) {
|
|
||||||
// 如果传入白色背景,添加灰色边框
|
|
||||||
it.border(
|
|
||||||
1.dp,
|
|
||||||
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
|
||||||
RoundedCornerShape(25.dp)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ fun ImageCropScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
// 用户取消图片选择,清除已裁剪的图片
|
|
||||||
AccountEditViewModel.croppedBitmap = null
|
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +103,6 @@ fun ImageCropScreen() {
|
|||||||
painter = painterResource(R.drawable.rider_pro_back_icon),
|
painter = painterResource(R.drawable.rider_pro_back_icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
// 用户取消头像选择,清除已裁剪的图片
|
|
||||||
AccountEditViewModel.croppedBitmap = null
|
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(Color.White)
|
colorFilter = ColorFilter.tint(Color.White)
|
||||||
@@ -123,11 +119,13 @@ fun ImageCropScreen() {
|
|||||||
val bitmap = it.onCrop()
|
val bitmap = it.onCrop()
|
||||||
|
|
||||||
// 专门处理个人资料头像
|
// 专门处理个人资料头像
|
||||||
// 只设置裁剪后的图片,不立即上传,等待用户在编辑资料界面点击保存
|
|
||||||
AccountEditViewModel.croppedBitmap = bitmap
|
AccountEditViewModel.croppedBitmap = bitmap
|
||||||
|
AccountEditViewModel.viewModelScope.launch {
|
||||||
|
AccountEditViewModel.updateUserProfile(context)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -17,19 +17,15 @@ 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.foundation.verticalScroll
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
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.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.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.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
|
||||||
@@ -46,7 +42,7 @@ 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.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@@ -88,11 +84,43 @@ fun FavouriteListPage() {
|
|||||||
var moments = dataFlow.collectAsLazyPagingItems()
|
var moments = dataFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
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))
|
||||||
|
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)
|
model.refreshPager(force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(moments.itemCount == 0) {
|
} else if(moments.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -106,8 +134,8 @@ fun FavouriteListPage() {
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.empty_img
|
id = if (com.aiosman.ravenow.AppState.darkMode) R.mipmap.invalid_dark
|
||||||
else R.mipmap.empty_img),
|
else R.mipmap.invalid_name_1),
|
||||||
contentDescription = "No favourites",
|
contentDescription = "No favourites",
|
||||||
modifier = Modifier.size(181.dp, 153.dp)
|
modifier = Modifier.size(181.dp, 153.dp)
|
||||||
)
|
)
|
||||||
@@ -127,17 +155,6 @@ fun FavouriteListPage() {
|
|||||||
) {
|
) {
|
||||||
items(moments.itemCount) { idx ->
|
items(moments.itemCount) { idx ->
|
||||||
val momentItem = moments[idx] ?: return@items
|
val momentItem = moments[idx] ?: return@items
|
||||||
// 获取缩略图URL:优先使用图片,如果没有图片则使用视频缩略图
|
|
||||||
val thumbnailUrl = when {
|
|
||||||
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
|
|
||||||
momentItem.videos != null && momentItem.videos.isNotEmpty() -> {
|
|
||||||
momentItem.videos.first().thumbnailUrl ?: momentItem.videos.first().thumbnailDirectUrl
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbnailUrl == null) return@items
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -152,17 +169,14 @@ fun FavouriteListPage() {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = thumbnailUrl,
|
imageUrl = momentItem.images[0].thumbnail,
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
|
if (momentItem.images.size > 1) {
|
||||||
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
|
|
||||||
|
|
||||||
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
.padding(top = 8.dp, end = 8.dp)
|
||||||
@@ -175,31 +189,6 @@ fun FavouriteListPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideoMoment) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp, end = 8.dp)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(
|
|
||||||
color = Color.Black.copy(alpha = 0.4f),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = "",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ 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
|
||||||
@@ -42,7 +40,7 @@ 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.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -76,11 +74,43 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
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)
|
model.loadData(userId, true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (users.itemCount == 0) {
|
} else if (users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -93,20 +123,25 @@ fun FollowerListScreen(userId: Int) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.frame_31),
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.frame_4
|
||||||
|
else R.mipmap.invalid_name_8),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(181.dp, 153.dp)
|
modifier = Modifier.size(181.dp, 153.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.awaiting_traveler),
|
text = "还没有人关注你呢",
|
||||||
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),
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
maxLines = 2,
|
androidx.compose.material.Text(
|
||||||
overflow = TextOverflow.Ellipsis
|
text = "试着发信号出来,某人就会被吸引啦~",
|
||||||
|
color = appColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,10 @@ 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.draw.clip
|
||||||
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
|
||||||
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
|
||||||
@@ -41,7 +38,7 @@ 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.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@@ -49,14 +46,13 @@ import com.aiosman.ravenow.utils.NetworkUtils
|
|||||||
* 关注消息列表
|
* 关注消息列表
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
fun FollowerNoticeScreen() {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
|
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
|
||||||
darkIcons = !AppState.darkMode,
|
darkIcons = !AppState.darkMode,
|
||||||
maskBoxBackgroundColor = AppColors.background,
|
maskBoxBackgroundColor = AppColors.background
|
||||||
includeStatusBarPadding = includeStatusBarPadding
|
|
||||||
) {
|
) {
|
||||||
val model = FollowerNoticeViewModel
|
val model = FollowerNoticeViewModel
|
||||||
var dataFlow = model.followerItemsFlow
|
var dataFlow = model.followerItemsFlow
|
||||||
@@ -68,11 +64,43 @@ fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
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)
|
model.reload(force = true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (followers.itemCount == 0) {
|
} else if (followers.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -85,21 +113,26 @@ fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.invalid_name_5),
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.frame_4
|
||||||
|
else R.mipmap.invalid_name_8),
|
||||||
contentDescription = "No Followers",
|
contentDescription = "No Followers",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 181.dp, height = 153.dp)
|
.size(width = 181.dp, height = 153.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(9.dp))
|
Spacer(modifier = Modifier.height(if (AppState.darkMode) 9.dp else 24.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.no_one_pinged_yet),
|
text = "还没有人关注你呢",
|
||||||
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),
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
maxLines = 2,
|
androidx.compose.material.Text(
|
||||||
overflow = TextOverflow.Ellipsis
|
text = "试着发信号出来,某人就会被吸引啦~",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,22 +174,9 @@ fun FollowItem(
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val followText = stringResource(R.string.followed_you)
|
Box(
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.padding(vertical = 16.dp)
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 左侧头像区域
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = avatar,
|
|
||||||
contentDescription = nickname,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
NavigationRoute.AccountProfile.route.replace(
|
NavigationRoute.AccountProfile.route.replace(
|
||||||
@@ -165,33 +185,24 @@ fun FollowItem(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
// 右侧内容区域
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context = context,
|
||||||
|
imageUrl = avatar,
|
||||||
|
contentDescription = nickname,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
|
||||||
text = nickname,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = followText,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.secondaryText,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (!isFollowing && userId != AppState.UserId) {
|
if (!isFollowing && userId != AppState.UserId) {
|
||||||
FollowButton(
|
FollowButton(
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ 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
|
||||||
@@ -40,7 +38,7 @@ 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.NetworkErrorContent
|
import com.aiosman.ravenow.ui.network.ReloadButton
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
|
|
||||||
@@ -78,11 +76,43 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
|
||||||
|
|
||||||
if (!isNetworkAvailable) {
|
if (!isNetworkAvailable) {
|
||||||
NetworkErrorContent(
|
Box(
|
||||||
onReload = {
|
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)
|
model.loadData(userId, true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(users.itemCount == 0) {
|
} else if(users.itemCount == 0) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -95,20 +125,25 @@ fun FollowingListScreen(userId: Int) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.frame_31),
|
painter = painterResource(
|
||||||
|
id = if(AppState.darkMode) R.mipmap.frame_3
|
||||||
|
else R.mipmap.invalid_name_9),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(181.dp, 153.dp)
|
modifier = Modifier.size(181.dp, 153.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
Spacer(modifier = Modifier.size(9.dp)) // 调整间距为9dp
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = stringResource(R.string.awaiting_traveler),
|
text = "还没有关注任何灵魂",
|
||||||
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),
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
maxLines = 2,
|
androidx.compose.material.Text(
|
||||||
overflow = TextOverflow.Ellipsis
|
text = "探索一下,总有一个你想靠近的光点 ✨",
|
||||||
|
color = appColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,592 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
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.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
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.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
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.graphics.SolidColor
|
|
||||||
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.input.TextFieldValue
|
|
||||||
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.CustomAsyncImage
|
|
||||||
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.modifiers.noRippleClickable
|
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun AddGroupMemberScreen(groupId: String, groupName: String?) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val systemUiController = rememberSystemUiController()
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
var searchText by remember { mutableStateOf(TextFieldValue("")) }
|
|
||||||
var selectedMembers by remember { mutableStateOf(listOf<GroupMember>()) }
|
|
||||||
var selectedMemberIds by remember { mutableStateOf<Set<String>>(emptySet()) }
|
|
||||||
var pagerState = rememberPagerState(pageCount = { 2 })
|
|
||||||
var scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
// LazyRow状态管理
|
|
||||||
val lazyRowState = rememberLazyListState()
|
|
||||||
|
|
||||||
// 清除错误信息
|
|
||||||
LaunchedEffect(searchText.text) {
|
|
||||||
if (AddGroupMemberViewModel.errorMessage != null) {
|
|
||||||
AddGroupMemberViewModel.clearError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听selectedMembers变化,当有新成员添加时自动滚动到最后一个
|
|
||||||
LaunchedEffect(selectedMembers.size) {
|
|
||||||
if (selectedMembers.isNotEmpty()) {
|
|
||||||
kotlinx.coroutines.delay(100)
|
|
||||||
lazyRowState.animateScrollToItem(selectedMembers.size - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
|
||||||
AddGroupMemberViewModel.groupName = groupName
|
|
||||||
AddGroupMemberViewModel.trtcId = groupId
|
|
||||||
AddGroupMemberViewModel.roomId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(AppColors.background)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
|
|
||||||
// 头部
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 返回按钮
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = "back",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_chat_info_add_member),
|
|
||||||
fontSize = 17.sp,
|
|
||||||
fontWeight = FontWeight.W700,
|
|
||||||
color = AppColors.text,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索栏(暂时不实现,但保留UI)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.background(
|
|
||||||
color = AppColors.inputBackground,
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 13.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_nav_search),
|
|
||||||
contentDescription = stringResource(R.string.search),
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
BasicTextField(
|
|
||||||
value = searchText,
|
|
||||||
onValueChange = { searchText = it },
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 14.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
singleLine = true,
|
|
||||||
cursorBrush = SolidColor(AppColors.text),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Box {
|
|
||||||
if (searchText.text.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.search),
|
|
||||||
color = AppColors.secondaryText,
|
|
||||||
fontSize = 14.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已选成员列表
|
|
||||||
if (selectedMembers.isNotEmpty()) {
|
|
||||||
LazyRow(
|
|
||||||
state = lazyRowState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(selectedMembers) { member ->
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.width(48.dp)
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
CustomAsyncImage(
|
|
||||||
context = context,
|
|
||||||
imageUrl = member.avatar,
|
|
||||||
contentDescription = member.name,
|
|
||||||
defaultRes = R.drawable.default_avatar,
|
|
||||||
placeholderRes = R.drawable.default_avatar,
|
|
||||||
errorRes = R.drawable.default_avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 删除按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.background(AppColors.error, CircleShape)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.noRippleClickable {
|
|
||||||
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.removeSelectedMember(
|
|
||||||
member, selectedMemberIds, selectedMembers
|
|
||||||
)
|
|
||||||
selectedMemberIds = newSelectedMemberIds
|
|
||||||
selectedMembers = newSelectedMembers
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "×",
|
|
||||||
color = AppColors.mainText,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 名称显示
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = if (member.name.length > 5) {
|
|
||||||
member.name.substring(0, 5) + "..."
|
|
||||||
} else {
|
|
||||||
member.name
|
|
||||||
},
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = AppColors.text,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab切换
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
TabItem(
|
|
||||||
text = stringResource(R.string.chat_ai),
|
|
||||||
isSelected = pagerState.currentPage == 0,
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TabSpacer()
|
|
||||||
TabItem(
|
|
||||||
text = stringResource(R.string.chat_friend),
|
|
||||||
isSelected = pagerState.currentPage == 1,
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区域 - 自适应填满剩余高度
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
when (it) {
|
|
||||||
0 -> {
|
|
||||||
// AI智能体列表
|
|
||||||
AddMemberAiAgentListScreen(
|
|
||||||
searchText = searchText.text,
|
|
||||||
selectedMemberIds = selectedMemberIds,
|
|
||||||
excludeRoomId = null,
|
|
||||||
onMemberSelect = { member ->
|
|
||||||
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.toggleMemberSelection(
|
|
||||||
member, selectedMemberIds, selectedMembers
|
|
||||||
)
|
|
||||||
selectedMemberIds = newSelectedMemberIds
|
|
||||||
selectedMembers = newSelectedMembers
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
// 朋友列表
|
|
||||||
AddMemberFriendListScreen(
|
|
||||||
searchText = searchText.text,
|
|
||||||
selectedMemberIds = selectedMemberIds,
|
|
||||||
roomId = null,
|
|
||||||
onMemberSelect = { member ->
|
|
||||||
val (newSelectedMemberIds, newSelectedMembers) = AddGroupMemberViewModel.toggleMemberSelection(
|
|
||||||
member, selectedMemberIds, selectedMembers
|
|
||||||
)
|
|
||||||
selectedMemberIds = newSelectedMemberIds
|
|
||||||
selectedMembers = newSelectedMembers
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认按钮 - 固定在底部
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (selectedMembers.isNotEmpty()) {
|
|
||||||
scope.launch {
|
|
||||||
val success = AddGroupMemberViewModel.addMembersToGroup(selectedMembers)
|
|
||||||
if (success) {
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = AppColors.main,
|
|
||||||
contentColor = AppColors.mainText,
|
|
||||||
disabledContainerColor = AppColors.disabledBackground,
|
|
||||||
disabledContentColor = AppColors.text
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
enabled = selectedMembers.isNotEmpty() && !AddGroupMemberViewModel.isLoading
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.lets_ride_upper),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 居中显示的错误提示弹窗
|
|
||||||
AddGroupMemberViewModel.errorMessage?.let { error ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.Center),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
androidx.compose.material3.Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.8f),
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = error,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
color = Color.Red,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持excludeRoomId的AI智能体列表Screen
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
|
||||||
fun AddMemberAiAgentListScreen(
|
|
||||||
searchText: String,
|
|
||||||
selectedMemberIds: Set<String> = emptySet(),
|
|
||||||
excludeRoomId: Int? = null,
|
|
||||||
onMemberSelect: (GroupMember) -> Unit
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
val viewModel = remember(excludeRoomId) { AddMemberAiAgentListViewModel(excludeRoomId) }
|
|
||||||
|
|
||||||
val filteredAgents = viewModel.getFilteredAgents(searchText)
|
|
||||||
|
|
||||||
val pullRefreshState = rememberPullRefreshState(
|
|
||||||
refreshing = viewModel.isRefreshing,
|
|
||||||
onRefresh = {
|
|
||||||
viewModel.refresh(searchText)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
|
||||||
.collect { visibleItems ->
|
|
||||||
if (visibleItems.isNotEmpty()) {
|
|
||||||
val lastVisibleItem = visibleItems.last()
|
|
||||||
if (lastVisibleItem.index >= filteredAgents.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
|
|
||||||
viewModel.loadMore(searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pullRefresh(pullRefreshState)
|
|
||||||
) {
|
|
||||||
if (viewModel.isLoading && filteredAgents.isEmpty()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
LottieAnimation(
|
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
|
||||||
iterations = LottieConstants.IterateForever,
|
|
||||||
modifier = Modifier.size(80.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = filteredAgents,
|
|
||||||
key = { it.id }
|
|
||||||
) { agent ->
|
|
||||||
MemberItem(
|
|
||||||
member = agent,
|
|
||||||
isSelected = selectedMemberIds.contains(agent.id),
|
|
||||||
onSelect = { onMemberSelect(agent) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.isLoadingMore) {
|
|
||||||
item {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
LottieAnimation(
|
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
|
||||||
iterations = LottieConstants.IterateForever,
|
|
||||||
modifier = Modifier.size(80.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PullRefreshIndicator(
|
|
||||||
refreshing = viewModel.isRefreshing,
|
|
||||||
state = pullRefreshState,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
backgroundColor = AppColors.background,
|
|
||||||
contentColor = AppColors.main
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持roomId的朋友列表Screen
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
|
||||||
fun AddMemberFriendListScreen(
|
|
||||||
searchText: String,
|
|
||||||
selectedMemberIds: Set<String> = emptySet(),
|
|
||||||
roomId: Int? = null,
|
|
||||||
onMemberSelect: (GroupMember) -> Unit
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
val viewModel = remember(roomId) { AddMemberFriendListViewModel(roomId) }
|
|
||||||
|
|
||||||
val filteredFriends = viewModel.getFilteredFriends(searchText)
|
|
||||||
|
|
||||||
val pullRefreshState = rememberPullRefreshState(
|
|
||||||
refreshing = viewModel.isRefreshing,
|
|
||||||
onRefresh = {
|
|
||||||
viewModel.refresh(searchText)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
|
||||||
.collect { visibleItems ->
|
|
||||||
if (visibleItems.isNotEmpty()) {
|
|
||||||
val lastVisibleItem = visibleItems.last()
|
|
||||||
if (lastVisibleItem.index >= filteredFriends.size - 3 && viewModel.hasMoreData && !viewModel.isLoadingMore && !viewModel.isLoading) {
|
|
||||||
viewModel.loadMore(searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pullRefresh(pullRefreshState)
|
|
||||||
) {
|
|
||||||
if (viewModel.isLoading && filteredFriends.isEmpty()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
LottieAnimation(
|
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
|
||||||
iterations = LottieConstants.IterateForever,
|
|
||||||
modifier = Modifier.size(80.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = filteredFriends,
|
|
||||||
key = { it.id }
|
|
||||||
) { friend ->
|
|
||||||
MemberItem(
|
|
||||||
member = friend,
|
|
||||||
isSelected = selectedMemberIds.contains(friend.id),
|
|
||||||
onSelect = { onMemberSelect(friend) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewModel.isLoadingMore) {
|
|
||||||
item {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
LottieAnimation(
|
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
|
||||||
iterations = LottieConstants.IterateForever,
|
|
||||||
modifier = Modifier.size(80.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PullRefreshIndicator(
|
|
||||||
refreshing = viewModel.isRefreshing,
|
|
||||||
state = pullRefreshState,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
backgroundColor = AppColors.background,
|
|
||||||
contentColor = AppColors.main
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.aiosman.ravenow.AppStore
|
|
||||||
import com.aiosman.ravenow.data.AccountService
|
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.RoomService
|
|
||||||
import com.aiosman.ravenow.data.RoomServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
object AddGroupMemberViewModel : ViewModel() {
|
|
||||||
val accountService: AccountService = AccountServiceImpl()
|
|
||||||
val userService: UserService = UserServiceImpl()
|
|
||||||
val roomService: RoomService = RoomServiceImpl()
|
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
|
||||||
var groupName: String? = null
|
|
||||||
var trtcId: String? = null
|
|
||||||
var roomId: Int? = null
|
|
||||||
|
|
||||||
// 添加成员到群聊
|
|
||||||
suspend fun addMembersToGroup(
|
|
||||||
selectedMembers: List<GroupMember>
|
|
||||||
): Boolean {
|
|
||||||
return try {
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
// 验证房间标识
|
|
||||||
if (trtcId == null && roomId == null) {
|
|
||||||
isLoading = false
|
|
||||||
val errorMsg = "房间标识不能为空"
|
|
||||||
showToast(errorMsg)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据isAi属性分别获取用户和智能体的OpenID列表
|
|
||||||
val userOpenIds = selectedMembers.filter { !it.isAi }.map { it.id }
|
|
||||||
val agentOpenIds = selectedMembers.filter { it.isAi }.map { it.id }
|
|
||||||
|
|
||||||
var allSuccess = true
|
|
||||||
var errorMessages = mutableListOf<String>()
|
|
||||||
|
|
||||||
// 添加用户到房间
|
|
||||||
if (userOpenIds.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
val userResult = roomService.addUserToRoom(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
openIds = userOpenIds
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查添加结果
|
|
||||||
if (userResult.failedCount > 0) {
|
|
||||||
val failedUsers = userResult.failedItems.joinToString(", ") { it.userId }
|
|
||||||
errorMessages.add("部分用户添加失败: $failedUsers")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userResult.successCount == 0 && userResult.failedCount > 0) {
|
|
||||||
allSuccess = false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
allSuccess = false
|
|
||||||
errorMessages.add("添加用户失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加智能体到房间
|
|
||||||
if (agentOpenIds.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
val agentResult = roomService.addAgentToRoom(
|
|
||||||
roomId = roomId,
|
|
||||||
trtcId = trtcId,
|
|
||||||
agentOpenIds = agentOpenIds
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查添加结果
|
|
||||||
if (agentResult.failedCount > 0) {
|
|
||||||
val failedAgents = agentResult.failedItems.joinToString(", ") { it.agentOpenId }
|
|
||||||
errorMessages.add("部分智能体添加失败: $failedAgents")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agentResult.successCount == 0 && agentResult.failedCount > 0) {
|
|
||||||
allSuccess = false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
allSuccess = false
|
|
||||||
errorMessages.add("添加智能体失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = false
|
|
||||||
|
|
||||||
if (allSuccess) {
|
|
||||||
if (errorMessages.isNotEmpty()) {
|
|
||||||
// 有部分失败,但至少有一些成功
|
|
||||||
showToast(errorMessages.joinToString("\n"))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
// 全部失败
|
|
||||||
val errorMsg = if (errorMessages.isNotEmpty()) {
|
|
||||||
errorMessages.joinToString("\n")
|
|
||||||
} else {
|
|
||||||
"添加成员失败"
|
|
||||||
}
|
|
||||||
showToast(errorMsg)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
isLoading = false
|
|
||||||
val errorMsg = "添加成员失败: ${e.message}"
|
|
||||||
showToast(errorMsg)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showToast(message: String) {
|
|
||||||
Log.w("AddGroupMemberViewModel", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除错误信息
|
|
||||||
fun clearError() {
|
|
||||||
errorMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加成员到选中列表
|
|
||||||
fun addSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
|
||||||
val newSelectedMemberIds = selectedMemberIds + member.id
|
|
||||||
val newSelectedMembers = if (selectedMembers.none { it.id == member.id }) {
|
|
||||||
selectedMembers + member
|
|
||||||
} else {
|
|
||||||
selectedMembers
|
|
||||||
}
|
|
||||||
return Pair(newSelectedMemberIds, newSelectedMembers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从选中列表移除成员
|
|
||||||
fun removeSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
|
||||||
val newSelectedMemberIds = selectedMemberIds - member.id
|
|
||||||
val newSelectedMembers = selectedMembers.filter { it.id != member.id }
|
|
||||||
return Pair(newSelectedMemberIds, newSelectedMembers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换成员选中状态
|
|
||||||
fun toggleMemberSelection(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
|
|
||||||
return if (selectedMemberIds.contains(member.id)) {
|
|
||||||
removeSelectedMember(member, selectedMemberIds, selectedMembers)
|
|
||||||
} else {
|
|
||||||
addSelectedMember(member, selectedMemberIds, selectedMembers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持roomId的AI智能体列表ViewModel
|
|
||||||
class AddMemberAiAgentListViewModel(private val excludeRoomId: Int? = null) : ViewModel() {
|
|
||||||
private val accountService: AccountService = AccountServiceImpl()
|
|
||||||
|
|
||||||
var aiAgents by mutableStateOf<List<GroupMember>>(emptyList())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isLoadingMore by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var currentPage by mutableStateOf(1)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var hasMoreData by mutableStateOf(true)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val pageSize = 20
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadAgents(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadAgents(page: Int, isRefresh: Boolean = false, searchText: String = "") {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
if (isRefresh) {
|
|
||||||
isRefreshing = true
|
|
||||||
} else if (page == 1) {
|
|
||||||
isLoading = true
|
|
||||||
} else {
|
|
||||||
isLoadingMore = true
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage = null
|
|
||||||
|
|
||||||
val response = accountService.getAgent(page, pageSize, excludeRoomId = excludeRoomId, title = if (searchText.isNotEmpty()) searchText else null, desc = if (searchText.isNotEmpty()) searchText else null)
|
|
||||||
if (response.isSuccessful && response.body() != null) {
|
|
||||||
val agentData = response.body()!!
|
|
||||||
val newAgents: List<GroupMember> = agentData.data.list.map { agent ->
|
|
||||||
GroupMember(
|
|
||||||
id = agent.openId,
|
|
||||||
name = agent.title,
|
|
||||||
avatar = "${ApiClient.BASE_API_URL+"/outside"}${agent.avatar}"+"?token="+"${AppStore.token}",
|
|
||||||
isAi = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRefresh || page == 1) {
|
|
||||||
aiAgents = newAgents
|
|
||||||
currentPage = 1
|
|
||||||
} else {
|
|
||||||
aiAgents = aiAgents + newAgents
|
|
||||||
currentPage = page
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreData = newAgents.size >= pageSize
|
|
||||||
} else {
|
|
||||||
errorMessage = "获取AI智能体列表失败: ${response.message()}"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = "获取AI智能体列表失败: ${e.message}"
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
isRefreshing = false
|
|
||||||
isLoadingMore = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh(searchText: String = "") {
|
|
||||||
loadAgents(1, true, searchText)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadMore(searchText: String = "") {
|
|
||||||
if (hasMoreData && !isLoadingMore && !isLoading) {
|
|
||||||
loadAgents(currentPage + 1, false, searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearError() {
|
|
||||||
errorMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFilteredAgents(searchText: String): List<GroupMember> {
|
|
||||||
return if (searchText.isEmpty()) {
|
|
||||||
aiAgents
|
|
||||||
} else {
|
|
||||||
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支持roomId的朋友列表ViewModel
|
|
||||||
class AddMemberFriendListViewModel(private val roomId: Int? = null) : ViewModel() {
|
|
||||||
private val userService: UserService = UserServiceImpl()
|
|
||||||
|
|
||||||
var friends by mutableStateOf<List<GroupMember>>(emptyList())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var isLoadingMore by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var currentPage by mutableStateOf(1)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var hasMoreData by mutableStateOf(true)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val pageSize = 20
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadFriends(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFriends(page: Int, isRefresh: Boolean = false, searchText: String = "") {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
if (isRefresh) {
|
|
||||||
isRefreshing = true
|
|
||||||
} else if (page == 1) {
|
|
||||||
isLoading = true
|
|
||||||
} else {
|
|
||||||
isLoadingMore = true
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage = null
|
|
||||||
|
|
||||||
val userData = userService.getUsers(pageSize, page, nickname = if (searchText.isNotEmpty()) searchText else null, roomId = roomId)
|
|
||||||
val newFriends: List<GroupMember> = userData.list.map { user ->
|
|
||||||
GroupMember(
|
|
||||||
id = user.chatAIId,
|
|
||||||
name = user.nickName,
|
|
||||||
avatar = user.avatar,
|
|
||||||
isAi = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRefresh || page == 1) {
|
|
||||||
friends = newFriends
|
|
||||||
currentPage = 1
|
|
||||||
} else {
|
|
||||||
friends = friends + newFriends
|
|
||||||
currentPage = page
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreData = newFriends.size >= pageSize
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = "获取朋友列表失败: ${e.message}"
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
isRefreshing = false
|
|
||||||
isLoadingMore = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh(searchText: String = "") {
|
|
||||||
loadFriends(1, true, searchText)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadMore(searchText: String = "") {
|
|
||||||
if (hasMoreData && !isLoadingMore && !isLoading) {
|
|
||||||
loadFriends(currentPage + 1, false, searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearError() {
|
|
||||||
errorMessage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFilteredFriends(searchText: String): List<GroupMember> {
|
|
||||||
return if (searchText.isEmpty()) {
|
|
||||||
friends
|
|
||||||
} else {
|
|
||||||
friends.filter { it.name.contains(searchText, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -11,10 +11,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
|
||||||
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
|
||||||
@@ -82,10 +78,10 @@ fun AiAgentListScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
Text(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
text = "加载中...",
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.secondaryText,
|
||||||
modifier = Modifier.size(80.dp)
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -115,10 +111,10 @@ fun AiAgentListScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
Text(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
text = "加载更多...",
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.secondaryText,
|
||||||
modifier = Modifier.size(80.dp)
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.aiosman.ravenow.ui.group
|
|||||||
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.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -15,15 +14,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
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.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
@@ -32,15 +27,11 @@ 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.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
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 androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.LocalNavController
|
import com.aiosman.ravenow.LocalNavController
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||||
import com.aiosman.ravenow.ui.composables.TabItem
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
@@ -99,28 +90,6 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
var showSelectTipsDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 自动隐藏“请选择群成员并输入群名称”提示弹窗
|
|
||||||
LaunchedEffect(showSelectTipsDialog) {
|
|
||||||
if (showSelectTipsDialog) {
|
|
||||||
kotlinx.coroutines.delay(2000)
|
|
||||||
showSelectTipsDialog = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取费用和余额信息
|
|
||||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
|
||||||
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
|
|
||||||
val roomMaxMembers by PointService.roomMaxMembers.collectAsState(initial = null)
|
|
||||||
|
|
||||||
val cost = CreateGroupChatViewModel.getCreateRoomCost()
|
|
||||||
val currentBalance = CreateGroupChatViewModel.getCurrentBalance()
|
|
||||||
val balanceAfterCost = CreateGroupChatViewModel.calculateBalanceAfterCost(cost)
|
|
||||||
val isBalanceSufficient = CreateGroupChatViewModel.isBalanceSufficient(cost)
|
|
||||||
|
|
||||||
// 获取群聊初始上限
|
|
||||||
val maxMemberLimit = roomMaxMembers?.defaultMaxTotal ?: 5
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||||
@@ -398,19 +367,14 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab切换和成员数量显示
|
// Tab切换
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Tab左对齐
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.chat_ai),
|
text = stringResource(R.string.chat_ai),
|
||||||
@@ -433,15 +397,6 @@ fun CreateGroupChatScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成员数量显示右对齐(x/x格式)
|
|
||||||
Text(
|
|
||||||
text = "${selectedMembers.size}/$maxMemberLimit",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = if (selectedMembers.size > maxMemberLimit) AppColors.error else AppColors.secondaryText,
|
|
||||||
fontWeight = FontWeight.W500
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区域 - 自适应填满剩余高度
|
// 内容区域 - 自适应填满剩余高度
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -481,78 +436,11 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 余额和扣减积分显示(创建按钮上方)
|
// 创建群聊按钮 - 固定在底部
|
||||||
val buttonTopPadding = if (cost > 0) 4.dp else 16.dp
|
|
||||||
if (cost > 0) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.create_group_chat_current_balance)}: ${currentBalance.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = AppColors.secondaryText
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.create_group_chat_required_cost)} ${cost.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color = AppColors.secondaryText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建群聊按钮 - 固定在底部(启用时使用渐变背景)
|
|
||||||
val isCreateEnabled =
|
|
||||||
groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
start = 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = buttonTopPadding,
|
|
||||||
bottom = navigationBarPadding + 16.dp
|
|
||||||
)
|
|
||||||
.let { baseModifier ->
|
|
||||||
if (isCreateEnabled) {
|
|
||||||
baseModifier.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0x997c45ed),
|
|
||||||
Color(0x997c68ef),
|
|
||||||
Color(0x997bd8f8)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
baseModifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
// 创建群聊逻辑
|
// 创建群聊逻辑
|
||||||
if (selectedMembers.isNotEmpty()) {
|
if (selectedMembers.isNotEmpty()) {
|
||||||
// 检查是否超过上限
|
|
||||||
if (selectedMembers.size > maxMemberLimit) {
|
|
||||||
CreateGroupChatViewModel.showError(
|
|
||||||
context.getString(
|
|
||||||
R.string.create_group_chat_exceed_limit,
|
|
||||||
maxMemberLimit
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
// 如果费用大于0,显示确认弹窗
|
|
||||||
if (cost > 0) {
|
|
||||||
CreateGroupChatViewModel.showConfirmDialog()
|
|
||||||
} else {
|
|
||||||
// 费用为0,直接创建
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val success = CreateGroupChatViewModel.createGroupChat(
|
val success = CreateGroupChatViewModel.createGroupChat(
|
||||||
groupName = groupName.text,
|
groupName = groupName.text,
|
||||||
@@ -564,22 +452,22 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
|
containerColor = AppColors.main,
|
||||||
contentColor = AppColors.mainText,
|
contentColor = AppColors.mainText,
|
||||||
disabledContainerColor = AppColors.disabledBackground,
|
disabledContainerColor = AppColors.disabledBackground,
|
||||||
disabledContentColor = AppColors.text
|
disabledContentColor = AppColors.text
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
enabled = isCreateEnabled
|
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
|
||||||
) {
|
) {
|
||||||
if (CreateGroupChatViewModel.isLoading) {
|
if (CreateGroupChatViewModel.isLoading) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.agent_createing),
|
text = "创建中...",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.W600
|
fontWeight = FontWeight.W600
|
||||||
)
|
)
|
||||||
@@ -592,76 +480,6 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用状态下拦截点击并弹出提示
|
|
||||||
if (!isCreateEnabled) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.noRippleClickable {
|
|
||||||
showSelectTipsDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请选择群成员并输入群名称 提示弹窗
|
|
||||||
if (showSelectTipsDialog) {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = { showSelectTipsDialog = false },
|
|
||||||
properties = DialogProperties(dismissOnClickOutside = true, dismissOnBackPress = true)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = AppColors.background, shape = RoundedCornerShape(16.dp))
|
|
||||||
.padding(horizontal = 20.dp, vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.create_group_chat_select_members_and_name),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color(0xFF7C45ED),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消费确认弹窗
|
|
||||||
if (CreateGroupChatViewModel.showConfirmDialog) {
|
|
||||||
CreateGroupChatConfirmDialog(
|
|
||||||
cost = cost,
|
|
||||||
currentBalance = currentBalance,
|
|
||||||
balanceAfterCost = balanceAfterCost,
|
|
||||||
isBalanceSufficient = isBalanceSufficient,
|
|
||||||
onConfirm = {
|
|
||||||
// 检查是否超过上限
|
|
||||||
if (selectedMembers.size > maxMemberLimit) {
|
|
||||||
CreateGroupChatViewModel.hideConfirmDialog()
|
|
||||||
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
|
||||||
} else {
|
|
||||||
CreateGroupChatViewModel.hideConfirmDialog()
|
|
||||||
scope.launch {
|
|
||||||
val success = CreateGroupChatViewModel.createGroupChat(
|
|
||||||
groupName = groupName.text,
|
|
||||||
selectedMembers = selectedMembers,
|
|
||||||
context = context
|
|
||||||
)
|
|
||||||
if (success) {
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
CreateGroupChatViewModel.hideConfirmDialog()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 居中显示的错误提示弹窗
|
// 居中显示的错误提示弹窗
|
||||||
@@ -678,7 +496,7 @@ fun CreateGroupChatScreen() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
|
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
|
||||||
verticalArrangement = Arrangement.Center // 垂直居中
|
verticalArrangement = Arrangement.Center // 垂直居中
|
||||||
) {
|
) {
|
||||||
Card(
|
androidx.compose.material3.Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.8f),
|
.fillMaxWidth(0.8f),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
@@ -690,7 +508,7 @@ fun CreateGroupChatScreen() {
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = TextAlign.Center
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,219 +516,3 @@ fun CreateGroupChatScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建群聊消费确认弹窗
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CreateGroupChatConfirmDialog(
|
|
||||||
cost: Int,
|
|
||||||
currentBalance: Int,
|
|
||||||
balanceAfterCost: Int,
|
|
||||||
isBalanceSufficient: Boolean,
|
|
||||||
onConfirm: () -> Unit,
|
|
||||||
onCancel: () -> Unit
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onCancel,
|
|
||||||
properties = DialogProperties(
|
|
||||||
dismissOnBackPress = true,
|
|
||||||
dismissOnClickOutside = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.9f)
|
|
||||||
.padding(16.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// 硬币图标(使用文本代替,实际项目中可以使用图片资源)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(64.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFFFFD700), // 金色
|
|
||||||
Color(0xFFFFA500) // 橙色
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = CircleShape
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "pai",
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.create_group_chat_confirm_title),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// 需要消耗
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.create_group_chat_required_cost),
|
|
||||||
amount = cost,
|
|
||||||
AppColors = AppColors
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// 当前余额
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.create_group_chat_current_balance),
|
|
||||||
amount = currentBalance,
|
|
||||||
AppColors = AppColors
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// 消耗后余额
|
|
||||||
CostInfoRow(
|
|
||||||
label = stringResource(R.string.create_group_chat_balance_after),
|
|
||||||
amount = balanceAfterCost,
|
|
||||||
AppColors = AppColors
|
|
||||||
)
|
|
||||||
|
|
||||||
// 余额不足提示
|
|
||||||
if (!isBalanceSufficient) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.create_group_chat_insufficient_balance),
|
|
||||||
color = Color.Red,
|
|
||||||
fontSize = 14.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// 按钮行
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
// 取消按钮
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onCancel,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(48.dp)
|
|
||||||
.border(
|
|
||||||
width = 1.dp,
|
|
||||||
color = AppColors.secondaryText.copy(alpha = 0.3f),
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
|
||||||
contentColor = AppColors.text
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.cancel),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认消耗按钮
|
|
||||||
Button(
|
|
||||||
onClick = onConfirm,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(48.dp),
|
|
||||||
enabled = isBalanceSufficient,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = AppColors.main,
|
|
||||||
contentColor = AppColors.mainText,
|
|
||||||
disabledContainerColor = AppColors.disabledBackground,
|
|
||||||
disabledContentColor = AppColors.text
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.create_group_chat_confirm_consume),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.W600
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 费用信息行组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CostInfoRow(
|
|
||||||
label: String,
|
|
||||||
amount: Int,
|
|
||||||
AppColors: com.aiosman.ravenow.AppThemeData
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
// 小硬币图标(使用简单的圆形)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(16.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.linearGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFFFFD700),
|
|
||||||
Color(0xFFFFA500)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = CircleShape
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "${amount.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化数字,添加千位分隔符
|
|
||||||
*/
|
|
||||||
fun Int.formatNumber(): String {
|
|
||||||
return this.toString().reversed().chunked(3).joinToString(",").reversed()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ import com.aiosman.ravenow.ConstVars
|
|||||||
import com.aiosman.ravenow.data.AccountNotice
|
import com.aiosman.ravenow.data.AccountNotice
|
||||||
import com.aiosman.ravenow.data.AccountService
|
import com.aiosman.ravenow.data.AccountService
|
||||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import com.aiosman.ravenow.data.UserService
|
import com.aiosman.ravenow.data.UserService
|
||||||
import com.aiosman.ravenow.data.UserServiceImpl
|
import com.aiosman.ravenow.data.UserServiceImpl
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.ravenow.exp.formatChatTime
|
import com.aiosman.ravenow.exp.formatChatTime
|
||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
@@ -37,7 +35,6 @@ object CreateGroupChatViewModel : ViewModel() {
|
|||||||
// 状态管理
|
// 状态管理
|
||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
var showConfirmDialog by mutableStateOf(false)
|
|
||||||
|
|
||||||
// 创建群聊
|
// 创建群聊
|
||||||
suspend fun createGroupChat(
|
suspend fun createGroupChat(
|
||||||
@@ -58,13 +55,13 @@ object CreateGroupChatViewModel : ViewModel() {
|
|||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
val errorMsg = context.getString(R.string.create_group_chat_failed, response.message() ?: "")
|
val errorMsg = "创建群聊失败: ${response.message()}"
|
||||||
showToast(errorMsg)
|
showToast(errorMsg)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
val errorMsg = context.getString(R.string.create_group_chat_failed, e.message ?: "")
|
val errorMsg = "创建群聊失败: ${e.message}"
|
||||||
showToast(errorMsg)
|
showToast(errorMsg)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -78,11 +75,6 @@ object CreateGroupChatViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示错误信息(公开方法)
|
|
||||||
fun showError(message: String) {
|
|
||||||
showToast(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除错误信息
|
// 清除错误信息
|
||||||
fun clearError() {
|
fun clearError() {
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
@@ -114,59 +106,4 @@ object CreateGroupChatViewModel : ViewModel() {
|
|||||||
addSelectedMember(member, selectedMemberIds, selectedMembers)
|
addSelectedMember(member, selectedMemberIds, selectedMembers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取创建群聊的费用
|
|
||||||
* @return 费用金额,如果无法获取则返回 0
|
|
||||||
*/
|
|
||||||
fun getCreateRoomCost(): Int {
|
|
||||||
val rules = PointService.pointsRules.value
|
|
||||||
val costRule = rules?.sub?.get(PointService.PointsRuleKey.CREATE_ROOM)
|
|
||||||
return when (costRule) {
|
|
||||||
is PointService.RuleAmount.Fixed -> costRule.value
|
|
||||||
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
|
|
||||||
null -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前余额
|
|
||||||
* @return 当前余额,如果无法获取则返回 0
|
|
||||||
*/
|
|
||||||
fun getCurrentBalance(): Int {
|
|
||||||
return PointService.pointsBalance.value?.balance ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算消耗后余额
|
|
||||||
* @param cost 费用
|
|
||||||
* @return 消耗后余额
|
|
||||||
*/
|
|
||||||
fun calculateBalanceAfterCost(cost: Int): Int {
|
|
||||||
val currentBalance = getCurrentBalance()
|
|
||||||
return (currentBalance - cost).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查余额是否充足
|
|
||||||
* @param cost 费用
|
|
||||||
* @return 是否充足
|
|
||||||
*/
|
|
||||||
fun isBalanceSufficient(cost: Int): Boolean {
|
|
||||||
return getCurrentBalance() >= cost
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示确认弹窗
|
|
||||||
*/
|
|
||||||
fun showConfirmDialog() {
|
|
||||||
showConfirmDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏确认弹窗
|
|
||||||
*/
|
|
||||||
fun hideConfirmDialog() {
|
|
||||||
showConfirmDialog = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
|
||||||
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
|
||||||
@@ -82,10 +78,10 @@ fun FriendListScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
Text(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
text = "加载中...",
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.secondaryText,
|
||||||
modifier = Modifier.size(80.dp)
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -115,10 +111,10 @@ fun FriendListScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
Text(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
text = "加载更多...",
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.secondaryText,
|
||||||
modifier = Modifier.size(80.dp)
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.group
|
|||||||
|
|
||||||
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.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -41,13 +40,9 @@ 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.composables.CustomAsyncImage
|
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
|
|
||||||
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.index.NavItem
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.ravenow.ui.navigateToAddGroupMember
|
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupMembers
|
|
||||||
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -69,8 +64,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
var showAddMemoryDialog by remember { mutableStateOf(false) }
|
var showAddMemoryDialog by remember { mutableStateOf(false) }
|
||||||
var showMemoryManageDialog by remember { mutableStateOf(false) }
|
var showMemoryManageDialog by remember { mutableStateOf(false) }
|
||||||
var showVisibilityDialog by remember { mutableStateOf(false) }
|
var showVisibilityDialog by remember { mutableStateOf(false) }
|
||||||
var showVisibilityPaymentDialog by remember { mutableStateOf(false) }
|
|
||||||
var pendingIsPrivate by remember { mutableStateOf(false) }
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val memoryManageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val memoryManageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
@@ -175,7 +168,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}",
|
text = "${viewModel.groupInfo?.memberCount ?: 0}人",
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text.copy(alpha = 0.7f),
|
color = AppColors.text.copy(alpha = 0.7f),
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
@@ -189,17 +182,14 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
// 添加其他人
|
// 添加其他人
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
// TODO: 实现添加其他人功能
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -225,7 +215,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 通知设置
|
// 通知设置
|
||||||
Column(
|
Column(
|
||||||
@@ -264,11 +253,11 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分享群聊
|
// 退出群聊
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
// TODO: 实现分享功能
|
// TODO: 实现退出群聊功能
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@@ -278,7 +267,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icon_share),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
@@ -296,6 +285,27 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解锁群扩展 横幅
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(AppColors.decentBackground.copy(alpha = 0.35f))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_chat_info_unlock_extension),
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
color = AppColors.main,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 群记忆 卡片
|
// 群记忆 卡片
|
||||||
item {
|
item {
|
||||||
@@ -303,16 +313,16 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(AppColors.background)
|
.background(AppColors.decentBackground.copy(alpha = 0.28f))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icons_brain),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
@@ -321,6 +331,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
@@ -342,7 +353,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(AppColors.decentBackground)
|
.background(AppColors.background)
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showAddMemoryDialog = true
|
showAddMemoryDialog = true
|
||||||
@@ -361,7 +372,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(AppColors.decentBackground)
|
.background(AppColors.background)
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
showMemoryManageDialog = true
|
showMemoryManageDialog = true
|
||||||
@@ -384,21 +395,19 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(13.dp))
|
Spacer(modifier = Modifier.height(13.dp))
|
||||||
|
|
||||||
// 仅当当前用户是群聊创建者时显示以下组件
|
// 设置聊天主题
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
// 群资料设置
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToGroupProfileSettings(groupId)
|
// TODO: 实现设置聊天主题功能
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.fengm),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
@@ -446,8 +455,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
// 未解锁时才显示"待解锁"
|
|
||||||
if (viewModel.groupInfo?.privateFeePaid != true) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_locked),
|
text = stringResource(R.string.group_chat_info_locked),
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
@@ -455,14 +462,12 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 成员管理
|
// 成员管理
|
||||||
Row(
|
Row(
|
||||||
@@ -471,26 +476,20 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigateToGroupMembers(groupId)
|
// 静态占位
|
||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.icons_users),
|
painter = painterResource(R.drawable.group_info_users),
|
||||||
modifier = Modifier.size(25.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(R.string.group_chat_info_member_manage),
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
|
||||||
R.string.group_chat_info_member_manage
|
|
||||||
} else {
|
|
||||||
R.string.group_members_title
|
|
||||||
}
|
|
||||||
),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontSize = 15.sp
|
fontSize = 15.sp
|
||||||
@@ -513,8 +512,8 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.iconsgallery),
|
painter = painterResource(R.drawable.group_info_edit),
|
||||||
modifier = Modifier.size(25.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(
|
||||||
AppColors.text)
|
AppColors.text)
|
||||||
@@ -534,8 +533,7 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (viewModel.groupInfo?.isCreator == true) {
|
// 解散群聊
|
||||||
// 解散群聊(仅群主)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -544,14 +542,13 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
.noRippleClickable { },
|
.noRippleClickable { },
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(3.dp))
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.iconslogout),
|
painter = painterResource(R.drawable.group_info_exit),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(11.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_dissolve),
|
text = stringResource(R.string.group_chat_info_dissolve),
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
@@ -563,41 +560,9 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
painter = painterResource(R.drawable.rave_now_nav_right),
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 退出群聊(非群主)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.padding(12.dp)
|
|
||||||
.noRippleClickable { },
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.width(3.dp))
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.group_info_exit),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(11.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_chat_info_quit),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = Color(0xFFFF3B30),
|
|
||||||
fontSize = 15.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.rave_now_nav_right),
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -625,50 +590,11 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||||
) {
|
) {
|
||||||
GroupVisibilityDialog(
|
GroupVisibilityDialog(
|
||||||
viewModel = viewModel,
|
onDismiss = { showVisibilityDialog = false }
|
||||||
onDismiss = { showVisibilityDialog = false },
|
|
||||||
onConfirmPrivate = { isPrivate ->
|
|
||||||
// 如果选择私密群组且未解锁,显示付费确认弹框
|
|
||||||
if (isPrivate && (viewModel.groupInfo?.privateFeePaid != true)) {
|
|
||||||
pendingIsPrivate = true
|
|
||||||
showVisibilityDialog = false
|
|
||||||
showVisibilityPaymentDialog = true
|
|
||||||
} else {
|
|
||||||
// 直接更新可见性
|
|
||||||
viewModel.updateVisibility(isPrivate)
|
|
||||||
showVisibilityDialog = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 付费确认弹框
|
|
||||||
if (showVisibilityPaymentDialog) {
|
|
||||||
val cost = viewModel.privateGroupCost ?: 0
|
|
||||||
val currentBalance = viewModel.pointsBalance ?: 0
|
|
||||||
val balanceAfterCost = (currentBalance - cost).coerceAtLeast(0)
|
|
||||||
val isBalanceSufficient = currentBalance >= cost
|
|
||||||
|
|
||||||
PointsPaymentDialog(
|
|
||||||
cost = cost,
|
|
||||||
currentBalance = currentBalance,
|
|
||||||
balanceAfterCost = balanceAfterCost,
|
|
||||||
isBalanceSufficient = isBalanceSufficient,
|
|
||||||
onConfirm = {
|
|
||||||
// 确认支付,更新可见性
|
|
||||||
viewModel.updateVisibility(pendingIsPrivate)
|
|
||||||
showVisibilityPaymentDialog = false
|
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
showVisibilityPaymentDialog = false
|
|
||||||
},
|
|
||||||
title = stringResource(R.string.group_chat_info_private_group),
|
|
||||||
description = stringResource(R.string.group_chat_info_private_group_desc),
|
|
||||||
isProcessing = viewModel.isUpdatingVisibility
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加群记忆弹窗
|
// 添加群记忆弹窗
|
||||||
if (showAddMemoryDialog) {
|
if (showAddMemoryDialog) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
@@ -720,10 +646,6 @@ fun GroupChatInfoScreen(groupId: String) {
|
|||||||
},
|
},
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||||
) {
|
) {
|
||||||
// 立即展开到全屏,避免逐渐变高的动画
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
memoryManageSheetState.expand()
|
|
||||||
}
|
|
||||||
GroupMemoryManageContent(
|
GroupMemoryManageContent(
|
||||||
groupId = groupId,
|
groupId = groupId,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -956,13 +878,8 @@ fun AddGroupMemoryDialog(
|
|||||||
text = "⭐",
|
text = "⭐",
|
||||||
fontSize = 13.sp
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
val memoryCost = viewModel.addMemoryCost
|
|
||||||
Text(
|
Text(
|
||||||
text = if (memoryCost != null && memoryCost > 0) {
|
text = stringResource(R.string.group_chat_info_memory_cost),
|
||||||
"添加记忆需消耗 ${memoryCost}派币"
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.group_chat_info_memory_cost)
|
|
||||||
},
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = Color.Black
|
color = Color.Black
|
||||||
@@ -1061,16 +978,12 @@ fun AddGroupMemoryDialog(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupVisibilityDialog(
|
fun GroupVisibilityDialog(
|
||||||
viewModel: GroupChatInfoViewModel,
|
onDismiss: () -> Unit
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirmPrivate: (Boolean) -> Unit
|
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val currentTrtcType = viewModel.groupInfo?.trtcType ?: "Public"
|
var isPrivate by remember { mutableStateOf(false) }
|
||||||
val isPrivateFeePaid = viewModel.groupInfo?.privateFeePaid == true
|
val balance = 482
|
||||||
var isPrivate by remember { mutableStateOf(currentTrtcType == "Private") }
|
val unlockCost = 500
|
||||||
val balance = viewModel.pointsBalance ?: 0
|
|
||||||
val unlockCost = viewModel.privateGroupCost ?: 0
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1125,15 +1038,14 @@ fun GroupVisibilityDialog(
|
|||||||
VisibilityOptionItem(
|
VisibilityOptionItem(
|
||||||
title = stringResource(R.string.group_chat_info_private_group),
|
title = stringResource(R.string.group_chat_info_private_group),
|
||||||
desc = stringResource(R.string.group_chat_info_private_group_desc),
|
desc = stringResource(R.string.group_chat_info_private_group_desc),
|
||||||
badge = if (!isPrivateFeePaid && unlockCost > 0) "${unlockCost}派币" else null,
|
badge = stringResource(R.string.group_chat_info_private_group_cost),
|
||||||
selected = isPrivate,
|
selected = isPrivate,
|
||||||
onClick = { isPrivate = true }
|
onClick = { isPrivate = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 余额与费用(仅未解锁时显示)
|
// 余额与费用
|
||||||
if (!isPrivateFeePaid) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -1156,7 +1068,6 @@ fun GroupVisibilityDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
|
||||||
|
|
||||||
// 完成按钮
|
// 完成按钮
|
||||||
Box(
|
Box(
|
||||||
@@ -1173,9 +1084,7 @@ fun GroupVisibilityDialog(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.noRippleClickable {
|
.noRippleClickable { onDismiss() },
|
||||||
onConfirmPrivate(isPrivate)
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -1184,8 +1093,6 @@ fun GroupVisibilityDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅未解锁时显示充值提示
|
|
||||||
if (!isPrivateFeePaid) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_recharge_hint),
|
text = stringResource(R.string.group_chat_info_recharge_hint),
|
||||||
@@ -1194,7 +1101,6 @@ fun GroupVisibilityDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VisibilityOptionItem(
|
private fun VisibilityOptionItem(
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ 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.RoomService
|
import com.aiosman.ravenow.data.api.AgentRule
|
||||||
import com.aiosman.ravenow.data.RoomServiceImpl
|
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
||||||
|
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
||||||
|
import com.aiosman.ravenow.data.api.UpdateAgentRuleRequestBody
|
||||||
|
import com.aiosman.ravenow.data.api.CreateAgentRuleRequestBody
|
||||||
|
import com.aiosman.ravenow.data.api.AgentRule
|
||||||
|
import com.aiosman.ravenow.data.api.AgentRuleQuota
|
||||||
import com.aiosman.ravenow.data.parseErrorResponse
|
import com.aiosman.ravenow.data.parseErrorResponse
|
||||||
import com.aiosman.ravenow.data.PointService
|
|
||||||
import com.aiosman.ravenow.entity.RoomRuleEntity
|
|
||||||
import com.aiosman.ravenow.entity.RoomRuleQuotaEntity
|
|
||||||
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
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GroupChatInfoViewModel(
|
class GroupChatInfoViewModel(
|
||||||
@@ -36,26 +37,30 @@ class GroupChatInfoViewModel(
|
|||||||
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
val notificationStrategy get() = chatNotification?.strategy ?: "default"
|
||||||
|
|
||||||
// 记忆管理相关状态
|
// 记忆管理相关状态
|
||||||
var memoryQuota by mutableStateOf<RoomRuleQuotaEntity?>(null)
|
var memoryQuota by mutableStateOf<AgentRuleQuota?>(null)
|
||||||
var memoryList by mutableStateOf<List<RoomRuleEntity>>(emptyList())
|
var memoryList by mutableStateOf<List<AgentRule>>(emptyList())
|
||||||
var isLoadingMemory by mutableStateOf(false)
|
var isLoadingMemory by mutableStateOf(false)
|
||||||
var memoryError by mutableStateOf<String?>(null)
|
var memoryError by mutableStateOf<String?>(null)
|
||||||
|
var promptOpenId by mutableStateOf<String?>(null)
|
||||||
// 房间规则服务
|
|
||||||
private val roomService: RoomService = RoomServiceImpl()
|
|
||||||
|
|
||||||
// 群可见性相关状态
|
|
||||||
var privateGroupCost by mutableStateOf<Int?>(null)
|
|
||||||
var pointsBalance by mutableStateOf<Int?>(null)
|
|
||||||
var isLoadingVisibility by mutableStateOf(false)
|
|
||||||
var isUpdatingVisibility by mutableStateOf(false)
|
|
||||||
|
|
||||||
// 群记忆相关状态
|
|
||||||
var addMemoryCost by mutableStateOf<Int?>(null)
|
|
||||||
init {
|
init {
|
||||||
loadGroupInfo()
|
loadGroupInfo()
|
||||||
loadVisibilityInfo()
|
loadPromptOpenId()
|
||||||
loadMemoryCost()
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群聊中智能体的 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)
|
||||||
@@ -91,9 +96,7 @@ class GroupChatInfoViewModel(
|
|||||||
"${ApiClient.BASE_API_URL+"/outside"}${it.avatar}"+"?token="+"${AppStore.token}"
|
"${ApiClient.BASE_API_URL+"/outside"}${it.avatar}"+"?token="+"${AppStore.token}"
|
||||||
},
|
},
|
||||||
memberCount = room.userCount,
|
memberCount = room.userCount,
|
||||||
isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString(),
|
isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString()
|
||||||
trtcType = it.trtcType ?: "Public",
|
|
||||||
privateFeePaid = it.privateFeePaid ?: false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,27 +109,57 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加群记忆(房间规则)
|
* 添加群记忆
|
||||||
* @param memoryText 记忆内容
|
* @param memoryText 记忆内容
|
||||||
|
* @param promptOpenId 智能体的 OpenID(可选),如果不提供则从群聊信息中获取
|
||||||
*/
|
*/
|
||||||
fun addGroupMemory(memoryText: String) {
|
fun addGroupMemory(memoryText: String, promptOpenId: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
isAddingMemory = true
|
isAddingMemory = true
|
||||||
addMemoryError = null
|
addMemoryError = null
|
||||||
addMemorySuccess = false
|
addMemorySuccess = false
|
||||||
|
|
||||||
// 使用房间规则接口创建群记忆
|
// 如果没有提供 promptOpenId,需要先获取群聊的智能体信息
|
||||||
roomService.createRoomRule(
|
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,
|
rule = memoryText,
|
||||||
trtcId = groupId
|
openId = openId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val response = ApiClient.api.createAgentRule(requestBody)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
addMemorySuccess = true
|
addMemorySuccess = true
|
||||||
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
|
Log.d("GroupChatInfoViewModel", "群记忆添加成功")
|
||||||
// 刷新记忆列表和配额
|
// 刷新记忆列表和配额
|
||||||
loadMemoryQuota()
|
loadMemoryQuota(openId)
|
||||||
loadMemoryList()
|
loadMemoryList(openId)
|
||||||
|
} else {
|
||||||
|
val errorResponse = parseErrorResponse(response.errorBody())
|
||||||
|
val errorMessage = errorResponse?.toServiceException()?.message
|
||||||
|
?: "添加群记忆失败: ${response.code()}"
|
||||||
|
throw Exception(errorMessage)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
addMemoryError = e.message ?: "添加群记忆失败"
|
addMemoryError = e.message ?: "添加群记忆失败"
|
||||||
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
|
Log.e("GroupChatInfoViewModel", "添加群记忆失败: ${e.message}", e)
|
||||||
@@ -137,16 +170,38 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取记忆配额信息(房间规则配额)
|
* 获取记忆配额信息
|
||||||
*/
|
*/
|
||||||
fun loadMemoryQuota() {
|
fun loadMemoryQuota(openId: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
isLoadingMemory = true
|
isLoadingMemory = true
|
||||||
memoryError = null
|
memoryError = null
|
||||||
|
|
||||||
// 使用房间规则接口获取配额
|
val targetOpenId = openId ?: promptOpenId
|
||||||
memoryQuota = roomService.getRoomRuleQuota(trtcId = groupId)
|
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) {
|
} catch (e: Exception) {
|
||||||
memoryError = e.message ?: "获取配额信息失败"
|
memoryError = e.message ?: "获取配额信息失败"
|
||||||
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
|
Log.e("GroupChatInfoViewModel", "获取配额信息失败: ${e.message}", e)
|
||||||
@@ -157,21 +212,38 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取记忆列表(房间规则列表)
|
* 获取记忆列表
|
||||||
*/
|
*/
|
||||||
fun loadMemoryList(page: Int = 1, pageSize: Int = 20) {
|
fun loadMemoryList(openId: String? = null, page: Int = 1, pageSize: Int = 20) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
isLoadingMemory = true
|
isLoadingMemory = true
|
||||||
memoryError = null
|
memoryError = null
|
||||||
|
|
||||||
// 使用房间规则接口获取列表
|
val targetOpenId = openId ?: promptOpenId
|
||||||
val result = roomService.getRoomRuleList(
|
if (targetOpenId.isNullOrBlank()) {
|
||||||
trtcId = groupId,
|
// 如果还没有获取到 openId,先获取
|
||||||
page = page,
|
val response = ApiClient.api.createGroupChatAi(trtcGroupId = groupId)
|
||||||
pageSize = pageSize
|
val groupChatResponse = response.body()?.data
|
||||||
)
|
val prompts = groupChatResponse?.prompts
|
||||||
memoryList = result.list
|
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) {
|
} catch (e: Exception) {
|
||||||
memoryError = e.message ?: "获取记忆列表失败"
|
memoryError = e.message ?: "获取记忆列表失败"
|
||||||
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
|
Log.e("GroupChatInfoViewModel", "获取记忆列表失败: ${e.message}", e)
|
||||||
@@ -182,7 +254,7 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除记忆(房间规则)
|
* 删除记忆
|
||||||
*/
|
*/
|
||||||
fun deleteMemory(ruleId: Int) {
|
fun deleteMemory(ruleId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -190,12 +262,19 @@ class GroupChatInfoViewModel(
|
|||||||
isLoadingMemory = true
|
isLoadingMemory = true
|
||||||
memoryError = null
|
memoryError = null
|
||||||
|
|
||||||
// 使用房间规则接口删除
|
val response = ApiClient.api.deleteAgentRule(ruleId)
|
||||||
roomService.deleteRoomRule(ruleId)
|
if (response.isSuccessful) {
|
||||||
|
|
||||||
// 刷新记忆列表和配额
|
// 刷新记忆列表和配额
|
||||||
loadMemoryQuota()
|
promptOpenId?.let { openId ->
|
||||||
loadMemoryList()
|
loadMemoryQuota(openId)
|
||||||
|
loadMemoryList(openId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val errorResponse = parseErrorResponse(response.errorBody())
|
||||||
|
val errorMessage = errorResponse?.toServiceException()?.message
|
||||||
|
?: "删除记忆失败: ${response.code()}"
|
||||||
|
throw Exception(errorMessage)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
memoryError = e.message ?: "删除记忆失败"
|
memoryError = e.message ?: "删除记忆失败"
|
||||||
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
|
Log.e("GroupChatInfoViewModel", "删除记忆失败: ${e.message}", e)
|
||||||
@@ -206,23 +285,34 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新记忆(房间规则)
|
* 更新记忆
|
||||||
*/
|
*/
|
||||||
fun updateMemory(ruleId: Int, newRuleText: String) {
|
fun updateMemory(ruleId: Int, newRuleText: String, targetOpenId: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
isLoadingMemory = true
|
isLoadingMemory = true
|
||||||
memoryError = null
|
memoryError = null
|
||||||
|
|
||||||
// 使用房间规则接口更新
|
val openId = targetOpenId ?: promptOpenId
|
||||||
roomService.updateRoomRule(
|
?: throw Exception("无法获取智能体ID")
|
||||||
|
|
||||||
|
val requestBody = UpdateAgentRuleRequestBody(
|
||||||
id = ruleId,
|
id = ruleId,
|
||||||
rule = newRuleText
|
rule = newRuleText,
|
||||||
|
openId = openId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val response = ApiClient.api.updateAgentRule(requestBody)
|
||||||
|
if (response.isSuccessful) {
|
||||||
// 刷新记忆列表和配额
|
// 刷新记忆列表和配额
|
||||||
loadMemoryQuota()
|
loadMemoryQuota(openId)
|
||||||
loadMemoryList()
|
loadMemoryList(openId)
|
||||||
|
} else {
|
||||||
|
val errorResponse = parseErrorResponse(response.errorBody())
|
||||||
|
val errorMessage = errorResponse?.toServiceException()?.message
|
||||||
|
?: "更新记忆失败: ${response.code()}"
|
||||||
|
throw Exception(errorMessage)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
memoryError = e.message ?: "更新记忆失败"
|
memoryError = e.message ?: "更新记忆失败"
|
||||||
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
|
Log.e("GroupChatInfoViewModel", "更新记忆失败: ${e.message}", e)
|
||||||
@@ -231,77 +321,4 @@ class GroupChatInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载群可见性相关信息(价格和积分余额)
|
|
||||||
*/
|
|
||||||
fun loadVisibilityInfo() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
isLoadingVisibility = true
|
|
||||||
|
|
||||||
// 获取积分规则中的私密群组价格
|
|
||||||
PointService.refreshPointsRules()
|
|
||||||
val rules = PointService.pointsRules.first()
|
|
||||||
val roomPrivateRule = rules?.sub?.get(PointService.PointsRuleKey.ROOM_PRIVATE)
|
|
||||||
privateGroupCost = when (roomPrivateRule) {
|
|
||||||
is PointService.RuleAmount.Fixed -> roomPrivateRule.value
|
|
||||||
is PointService.RuleAmount.Range -> roomPrivateRule.min
|
|
||||||
null -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取积分余额
|
|
||||||
PointService.refreshMyPointsBalance(includeStatistics = false)
|
|
||||||
val balance = PointService.pointsBalance.first()
|
|
||||||
pointsBalance = balance?.balance
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("GroupChatInfoViewModel", "加载可见性信息失败: ${e.message}", e)
|
|
||||||
} finally {
|
|
||||||
isLoadingVisibility = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新群可见性
|
|
||||||
* @param isPrivate 是否设置为私密群组
|
|
||||||
*/
|
|
||||||
fun updateVisibility(isPrivate: Boolean) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
isUpdatingVisibility = true
|
|
||||||
// TODO: 实现更新房间可见性的接口调用
|
|
||||||
// 暂时留空
|
|
||||||
|
|
||||||
// 更新成功后刷新群信息和积分余额
|
|
||||||
loadGroupInfo()
|
|
||||||
loadVisibilityInfo()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("GroupChatInfoViewModel", "更新可见性失败: ${e.message}", e)
|
|
||||||
} finally {
|
|
||||||
isUpdatingVisibility = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载添加群记忆的价格
|
|
||||||
*/
|
|
||||||
fun loadMemoryCost() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
// 获取积分规则中的房间记忆价格(群聊记忆,区别于 Agent 记忆)
|
|
||||||
PointService.refreshPointsRules()
|
|
||||||
val rules = PointService.pointsRules.first()
|
|
||||||
val roomMemoryRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_ROOM_MEMORY)
|
|
||||||
addMemoryCost = when (roomMemoryRule) {
|
|
||||||
is PointService.RuleAmount.Fixed -> roomMemoryRule.value
|
|
||||||
is PointService.RuleAmount.Range -> roomMemoryRule.min
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("GroupChatInfoViewModel", "加载记忆价格失败: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
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.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.aiosman.ravenow.LocalAppTheme
|
|
||||||
import com.aiosman.ravenow.LocalNavController
|
|
||||||
import com.aiosman.ravenow.R
|
|
||||||
import com.aiosman.ravenow.entity.GroupMember
|
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|
||||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
import com.aiosman.ravenow.ui.navigateToAddGroupMember
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupMembersScreen(groupId: String) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
val viewModel = viewModel<GroupMembersViewModel>(
|
|
||||||
key = "GroupMembersViewModel_$groupId",
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
return GroupMembersViewModel(groupId) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var selectedMemberPosition by remember { mutableStateOf<Pair<Offset, Float>?>(null) }
|
|
||||||
var selectedMemberId by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(AppColors.background)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
// 顶部导航栏
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(AppColors.background)
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
androidx.compose.foundation.Image(
|
|
||||||
painter = painterResource(R.drawable.rider_pro_back_icon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_members_title),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 17.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
// androidx.compose.foundation.Image(
|
|
||||||
// painter = painterResource(R.drawable.rider_pro_add_other),
|
|
||||||
// contentDescription = stringResource(R.string.group_chat_info_add_member),
|
|
||||||
// colorFilter = ColorFilter.tint(AppColors.text),
|
|
||||||
// modifier = Modifier
|
|
||||||
// .size(24.dp)
|
|
||||||
// .noRippleClickable {
|
|
||||||
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
|
||||||
) {
|
|
||||||
// 当前用户信息
|
|
||||||
item {
|
|
||||||
if (viewModel.currentUserMember != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_members_you),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text.copy(alpha = 0.6f),
|
|
||||||
fontSize = 13.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 12.dp, bottom = 8.dp)
|
|
||||||
)
|
|
||||||
CurrentUserItem(
|
|
||||||
member = viewModel.currentUserMember!!,
|
|
||||||
isAdmin = viewModel.groupInfo?.isCreator == true
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 群成员列表
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
R.string.group_members_list,
|
|
||||||
viewModel.members.size + (if (viewModel.currentUserMember != null) 1 else 0)
|
|
||||||
),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text.copy(alpha = 0.6f),
|
|
||||||
fontSize = 13.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(viewModel.members) { member ->
|
|
||||||
MemberItem(
|
|
||||||
member = member,
|
|
||||||
isAdmin = viewModel.groupInfo?.isCreator == true,
|
|
||||||
onSendMessage = {
|
|
||||||
// TODO: 实现发消息功能
|
|
||||||
},
|
|
||||||
onMenuClick = { position, height ->
|
|
||||||
if (selectedMemberId == member.userId) {
|
|
||||||
selectedMemberId = null
|
|
||||||
selectedMemberPosition = null
|
|
||||||
} else {
|
|
||||||
selectedMemberId = member.userId
|
|
||||||
selectedMemberPosition = Pair(position, height)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDeleteMember = {
|
|
||||||
viewModel.deleteMember(member.userId)
|
|
||||||
selectedMemberId = null
|
|
||||||
selectedMemberPosition = null
|
|
||||||
},
|
|
||||||
isMenuVisible = selectedMemberId == member.userId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 弹窗 - 显示在最上层
|
|
||||||
selectedMemberPosition?.let { (position, height) ->
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val screenWidth = with(density) { configuration.screenWidthDp.dp }
|
|
||||||
val horizontalOffset = (screenWidth - 238.dp) / 2
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(238.dp)
|
|
||||||
.height(60.dp)
|
|
||||||
.zIndex(1000f)
|
|
||||||
.offset {
|
|
||||||
val xOffset = with(density) { horizontalOffset.toPx().toInt() }
|
|
||||||
val yOffset = (position.y + height + with(density) { 4.dp.toPx() }).toInt()
|
|
||||||
IntOffset(x = xOffset, y = yOffset)
|
|
||||||
}
|
|
||||||
.shadow(
|
|
||||||
elevation = 8.dp,
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
spotColor = Color.Black.copy(alpha = 0.15f),
|
|
||||||
ambientColor = Color.Black.copy(alpha = 0.08f)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.background(
|
|
||||||
brush = androidx.compose.ui.graphics.Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF262626).copy(alpha = 0.4f),
|
|
||||||
Color(0xFFF5F5F5).copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.clickable {
|
|
||||||
selectedMemberId?.let { memberId ->
|
|
||||||
viewModel.deleteMember(memberId)
|
|
||||||
selectedMemberId = null
|
|
||||||
selectedMemberPosition = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_members_delete_member),
|
|
||||||
color = Color(0xFFFF3B30),
|
|
||||||
fontSize = 15.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CurrentUserItem(
|
|
||||||
member: GroupMember,
|
|
||||||
isAdmin: Boolean
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 头像
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
if (member.avatar.isNotEmpty()) Color.Transparent
|
|
||||||
else AppColors.decentBackground
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (member.avatar.isNotEmpty()) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = member.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentDescription = member.nickname
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 默认头像占位
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color(0xFFF0EEF1)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.default_avatar),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.text.copy(alpha = 0.5f),
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
// 名称和身份
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = member.nickname,
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (isAdmin) {
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_members_admin),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MemberItem(
|
|
||||||
member: GroupMember,
|
|
||||||
isAdmin: Boolean,
|
|
||||||
onSendMessage: () -> Unit,
|
|
||||||
onMenuClick: (Offset, Float) -> Unit,
|
|
||||||
onDeleteMember: () -> Unit,
|
|
||||||
isMenuVisible: Boolean
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val density = LocalDensity.current
|
|
||||||
var itemPosition by remember { mutableStateOf(Offset.Zero) }
|
|
||||||
var itemHeight by remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
itemPosition = coordinates.positionInRoot()
|
|
||||||
itemHeight = coordinates.size.height.toFloat()
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 头像
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
if (member.avatar.isNotEmpty()) Color.Transparent
|
|
||||||
else AppColors.decentBackground
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (member.avatar.isNotEmpty()) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = member.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(CircleShape),
|
|
||||||
contentDescription = member.nickname
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 默认头像占位
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color(0xFFF0EEF1)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.mipmap.group_copy),
|
|
||||||
contentDescription = "默认头像",
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
// 名称
|
|
||||||
Text(
|
|
||||||
text = member.nickname,
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// 菜单按钮
|
|
||||||
if (isAdmin) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { onMenuClick(itemPosition, itemHeight) },
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
) {
|
|
||||||
androidx.compose.foundation.Image(
|
|
||||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(AppColors.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
// 发消息按钮 - 右对齐到与Switch相同的位置
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(14.dp))
|
|
||||||
.background(AppColors.decentBackground)
|
|
||||||
.clickable { onSendMessage() }
|
|
||||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_members_send_message),
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.aiosman.ravenow.AppStore
|
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
|
||||||
import com.aiosman.ravenow.entity.GroupInfo
|
|
||||||
import com.aiosman.ravenow.entity.GroupMember
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class GroupMembersViewModel(
|
|
||||||
private val groupId: String
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
|
||||||
var currentUserMember by mutableStateOf<GroupMember?>(null)
|
|
||||||
var members by mutableStateOf<List<GroupMember>>(emptyList())
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var error by mutableStateOf<String?>(null)
|
|
||||||
var requireApproval by mutableStateOf(true) // 默认开启
|
|
||||||
var roomId by mutableStateOf<Int?>(null)
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadGroupMembers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupMembers() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
isLoading = true
|
|
||||||
error = null
|
|
||||||
|
|
||||||
// 调用接口获取群聊详情
|
|
||||||
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
|
|
||||||
|
|
||||||
if (response.isSuccessful && response.body() != null) {
|
|
||||||
val room = response.body()!!.data
|
|
||||||
|
|
||||||
// 保存roomId
|
|
||||||
roomId = room.id
|
|
||||||
|
|
||||||
// 设置群信息
|
|
||||||
groupInfo = GroupInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
groupName = room.name,
|
|
||||||
groupAvatar = if (room.avatar.isNullOrEmpty()) {
|
|
||||||
val groupIdBase64 = android.util.Base64.encodeToString(
|
|
||||||
groupId.toByteArray(),
|
|
||||||
android.util.Base64.NO_WRAP
|
|
||||||
)
|
|
||||||
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
|
|
||||||
} else {
|
|
||||||
"${ApiClient.BASE_API_URL}/outside${room.avatar}?token=${AppStore.token}"
|
|
||||||
},
|
|
||||||
memberCount = room.userCount,
|
|
||||||
isCreator = room.creator.userId == MyProfileViewModel.profile?.id.toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
// 获取当前用户ID
|
|
||||||
val currentUserId = MyProfileViewModel.profile?.id.toString()
|
|
||||||
|
|
||||||
// 转换成员列表
|
|
||||||
val allMembers = room.users.map { user ->
|
|
||||||
val avatarUrl = if (user.profile.avatar.isNullOrEmpty()) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
// 与动态列表和关注列表一致,使用 BASE_SERVER 构建头像URL,不需要token
|
|
||||||
"${ApiClient.BASE_SERVER}${user.profile.avatar}"
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupMember(
|
|
||||||
userId = user.userId,
|
|
||||||
nickname = user.profile.nickname.ifEmpty { user.profile.username },
|
|
||||||
avatar = avatarUrl,
|
|
||||||
isOwner = user.userId == room.creator.userId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分离当前用户和其他成员
|
|
||||||
currentUserMember = allMembers.find { it.userId == currentUserId }
|
|
||||||
members = allMembers.filter { it.userId != currentUserId }
|
|
||||||
|
|
||||||
} else {
|
|
||||||
error = "获取群成员列表失败"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = e.message ?: "加载失败"
|
|
||||||
Log.e("GroupMembersViewModel", "加载群成员失败", e)
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
loadGroupMembers()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMember(memberId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
// TODO: 实现删除成员的API调用
|
|
||||||
// 删除成功后刷新列表
|
|
||||||
loadGroupMembers()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = e.message ?: "删除成员失败"
|
|
||||||
Log.e("GroupMembersViewModel", "删除成员失败", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -39,9 +39,6 @@ import androidx.compose.ui.graphics.SolidColor
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupMemoryManageContent(
|
fun GroupMemoryManageContent(
|
||||||
@@ -51,6 +48,9 @@ fun GroupMemoryManageContent(
|
|||||||
onDismiss: () -> Unit = {}
|
onDismiss: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val sheetHeight = screenHeight * 0.95f
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// 编辑记忆的状态 - 存储正在编辑的记忆ID
|
// 编辑记忆的状态 - 存储正在编辑的记忆ID
|
||||||
@@ -68,7 +68,8 @@ fun GroupMemoryManageContent(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
|
.height(sheetHeight)
|
||||||
.background(Color(0xFFFAF9FB))
|
.background(Color(0xFFFAF9FB))
|
||||||
) {
|
) {
|
||||||
// 顶部栏:返回按钮 + 标题 + 加号按钮
|
// 顶部栏:返回按钮 + 标题 + 加号按钮
|
||||||
@@ -80,7 +81,7 @@ fun GroupMemoryManageContent(
|
|||||||
) {
|
) {
|
||||||
// 中间标题 - 绝对居中,不受其他组件影响
|
// 中间标题 - 绝对居中,不受其他组件影响
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.group_chat_info_memory_manage2),
|
text = "记忆管理",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
@@ -108,7 +109,7 @@ fun GroupMemoryManageContent(
|
|||||||
colorFilter = ColorFilter.tint(Color.Black)
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.back_upper),
|
text = "返回",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
@@ -149,7 +150,7 @@ fun GroupMemoryManageContent(
|
|||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(stringResource(R.string.memory_paid), style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
Text("已付费:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
||||||
Spacer(Modifier.width(3.dp))
|
Spacer(Modifier.width(3.dp))
|
||||||
Text(
|
Text(
|
||||||
"${quota?.purchasedCount ?: 0}",
|
"${quota?.purchasedCount ?: 0}",
|
||||||
@@ -157,7 +158,7 @@ fun GroupMemoryManageContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(stringResource(R.string.memory_used), style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
Text("已使用:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
||||||
Spacer(Modifier.width(3.dp))
|
Spacer(Modifier.width(3.dp))
|
||||||
Text(
|
Text(
|
||||||
"${quota?.currentCount ?: 0}",
|
"${quota?.currentCount ?: 0}",
|
||||||
@@ -166,7 +167,7 @@ fun GroupMemoryManageContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(stringResource(R.string.upper_limit), style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
Text("可用上限:", style = TextStyle(color = Color(0x993C3C43), fontSize = 13.sp))
|
||||||
Spacer(Modifier.width(3.dp))
|
Spacer(Modifier.width(3.dp))
|
||||||
Text(
|
Text(
|
||||||
"50",
|
"50",
|
||||||
@@ -236,12 +237,12 @@ fun GroupMemoryManageContent(
|
|||||||
|
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.no_memory),
|
text = "暂无记忆",
|
||||||
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
style = TextStyle(color = Color.Black, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.add_memory),
|
text = "点击上方按钮添加群记忆",
|
||||||
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
|
style = TextStyle(color = Color.Black, fontSize = 14.sp, fontWeight = FontWeight.Normal)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -256,7 +257,7 @@ fun GroupMemoryManageContent(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EditGroupMemoryDialog(
|
fun EditGroupMemoryDialog(
|
||||||
memory: com.aiosman.ravenow.entity.RoomRuleEntity,
|
memory: com.aiosman.ravenow.data.api.AgentRule,
|
||||||
viewModel: GroupChatInfoViewModel,
|
viewModel: GroupChatInfoViewModel,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onUpdateMemory: (String) -> Unit
|
onUpdateMemory: (String) -> Unit
|
||||||
@@ -402,7 +403,7 @@ fun EditGroupMemoryDialog(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MemoryItem(
|
fun MemoryItem(
|
||||||
memory: com.aiosman.ravenow.entity.RoomRuleEntity,
|
memory: com.aiosman.ravenow.data.api.AgentRule,
|
||||||
isEditing: Boolean = false,
|
isEditing: Boolean = false,
|
||||||
onEdit: () -> Unit = {},
|
onEdit: () -> Unit = {},
|
||||||
onCancel: () -> Unit = {},
|
onCancel: () -> Unit = {},
|
||||||
@@ -578,44 +579,20 @@ fun MemoryItem(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// 底部行:创建者信息 + 编辑删除按钮
|
// 底部行:日期 + 编辑删除按钮
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
// 创建者信息 - 左侧:头像 + 用户名 · 时间
|
// 日期文本 - 左侧
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 圆形头像
|
|
||||||
val avatarUrl = memory.creator?.avatarDirectUrl?.takeIf { it.isNotBlank() }
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = avatarUrl,
|
|
||||||
contentDescription = "创建者头像",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
defaultRes = R.drawable.default_avatar,
|
|
||||||
placeholderRes = R.drawable.default_avatar,
|
|
||||||
errorRes = R.drawable.default_avatar,
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
|
|
||||||
// 用户名 · 时间
|
|
||||||
Text(
|
Text(
|
||||||
text = buildString {
|
text = formattedDate,
|
||||||
append(memory.creator?.nickname ?: "未知用户")
|
|
||||||
append(" · ")
|
|
||||||
append(formattedDate)
|
|
||||||
},
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
color = Color(0x993C3C43),
|
color = Color(0x993C3C43),
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑和删除图标 - 右侧
|
// 编辑和删除图标 - 右侧
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.InputStream
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
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.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
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.TextStyle
|
|
||||||
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 androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
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.StatusBarSpacer
|
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupProfileSettingsScreen(groupId: String) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
|
|
||||||
val viewModel = viewModel<GroupProfileSettingsViewModel>(
|
|
||||||
key = "GroupProfileSettingsViewModel_$groupId",
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
return GroupProfileSettingsViewModel(groupId) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 群图标选择器
|
|
||||||
val groupIconPicker = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.GetContent()
|
|
||||||
) { uri ->
|
|
||||||
uri?.let {
|
|
||||||
// 直接转换为 Bitmap 并显示,不进行裁剪
|
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val bitmap = uriToBitmap(context, it)
|
|
||||||
bitmap?.let { bmp ->
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
viewModel.setGroupIcon(bmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 群头像选择器
|
|
||||||
val groupAvatarPicker = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.GetContent()
|
|
||||||
) { uri ->
|
|
||||||
uri?.let {
|
|
||||||
// 直接转换为 Bitmap 并显示,不进行裁剪
|
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val bitmap = uriToBitmap(context, it)
|
|
||||||
bitmap?.let { bmp ->
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
viewModel.setGroupAvatar(bmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(AppColors.background)
|
|
||||||
) {
|
|
||||||
// 群默认图标图片区域(渐变背景)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(249.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF7C45ED),
|
|
||||||
Color(0xFFE91E63)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp)
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
groupIconPicker.launch("image/*")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// 显示选中的图标或默认渐变
|
|
||||||
viewModel.groupIconBitmap?.let { bitmap ->
|
|
||||||
Image(
|
|
||||||
bitmap = bitmap.asImageBitmap(),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顶部导航栏(显示在渐变背景上层)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
) {
|
|
||||||
StatusBarSpacer()
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.rider_pro_back_icon),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigateUp()
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(Color.White)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_info_edit),
|
|
||||||
style = TextStyle(
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 群头像区域
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 270.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(70.dp)
|
|
||||||
) {
|
|
||||||
// 群头像显示
|
|
||||||
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = viewModel.groupInfo!!.groupAvatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
|
||||||
contentDescription = "群聊头像"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(AppColors.decentBackground),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = viewModel.groupInfo?.groupName?.firstOrNull()?.toString() ?: "",
|
|
||||||
style = TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右下角加号按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.size(22.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
brush = Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF7C45ED),
|
|
||||||
Color(0xFF7BD8F8)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
groupAvatarPicker.launch("image/*")
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "+",
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// 群聊名称编辑框
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(AppColors.decentBackground)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.group_name)+":",
|
|
||||||
style = TextStyle(
|
|
||||||
color = AppColors.text.copy(alpha = 0.7f),
|
|
||||||
fontSize = 14.sp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
BasicTextField(
|
|
||||||
value = viewModel.groupName,
|
|
||||||
onValueChange = { viewModel.updateGroupName(it) },
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = AppColors.text,
|
|
||||||
fontSize = 15.sp
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(AppColors.text),
|
|
||||||
modifier = Modifier.padding(start = 70.dp),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// 保存按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 24.dp)
|
|
||||||
.height(50.dp)
|
|
||||||
.clip(RoundedCornerShape(25.dp))
|
|
||||||
.background(
|
|
||||||
brush = Brush.horizontalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF7C45ED),
|
|
||||||
Color(0xFF7BD8F8)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.noRippleClickable {
|
|
||||||
// TODO: 实现保存功能
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.save),
|
|
||||||
style = TextStyle(
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 17.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将 Uri 转换为 Bitmap 的辅助函数
|
|
||||||
fun uriToBitmap(context: android.content.Context, uri: Uri): Bitmap? {
|
|
||||||
return try {
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
|
||||||
BitmapFactory.decodeStream(inputStream)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.aiosman.ravenow.ui.group
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.aiosman.ravenow.entity.GroupInfo
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class GroupProfileSettingsViewModel(
|
|
||||||
private val groupId: String
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
var groupInfo by mutableStateOf<GroupInfo?>(null)
|
|
||||||
var groupName by mutableStateOf("")
|
|
||||||
var groupIconBitmap by mutableStateOf<Bitmap?>(null)
|
|
||||||
var groupAvatarBitmap by mutableStateOf<Bitmap?>(null)
|
|
||||||
var isLoading by mutableStateOf(false)
|
|
||||||
var error by mutableStateOf<String?>(null)
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadGroupInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadGroupInfo() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
isLoading = true
|
|
||||||
error = null
|
|
||||||
// 直接调用 API 加载群信息
|
|
||||||
val response = com.aiosman.ravenow.data.api.ApiClient.api.getRoomDetail(trtcId = groupId)
|
|
||||||
val room = response.body()?.data
|
|
||||||
groupInfo = room?.let {
|
|
||||||
com.aiosman.ravenow.entity.GroupInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
groupName = it.name,
|
|
||||||
groupAvatar = if (it.avatar.isNullOrEmpty()) {
|
|
||||||
val groupIdBase64 = android.util.Base64.encodeToString(
|
|
||||||
groupId.toByteArray(),
|
|
||||||
android.util.Base64.NO_WRAP
|
|
||||||
)
|
|
||||||
"${com.aiosman.ravenow.data.api.ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${com.aiosman.ravenow.AppStore.token}"
|
|
||||||
} else {
|
|
||||||
"${com.aiosman.ravenow.data.api.ApiClient.BASE_API_URL}/outside${it.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
|
|
||||||
},
|
|
||||||
memberCount = room.userCount,
|
|
||||||
isCreator = room.creator.userId == com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel.profile?.id.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
groupInfo?.let {
|
|
||||||
groupName = it.groupName
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error = e.message ?: "加载失败"
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateGroupName(newName: String) {
|
|
||||||
groupName = newName
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGroupIcon(bitmap: Bitmap) {
|
|
||||||
groupIconBitmap = bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGroupAvatar(bitmap: Bitmap) {
|
|
||||||
groupAvatarBitmap = bitmap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ fun CreateBottomSheet(
|
|||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
|
windowInsets = BottomSheetDefaults.windowInsets,
|
||||||
containerColor = appColors.background,
|
containerColor = appColors.background,
|
||||||
dragHandle = null,
|
dragHandle = null,
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ 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.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.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -66,7 +70,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
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.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
|
||||||
@@ -136,7 +139,6 @@ fun IndexScreen() {
|
|||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = drawerState.isOpen,
|
gesturesEnabled = drawerState.isOpen,
|
||||||
scrimColor = Color.Black.copy(alpha = 0.6f),
|
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
SideMenuContent(
|
SideMenuContent(
|
||||||
@@ -263,11 +265,12 @@ fun IndexScreen() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
.padding(0.dp),
|
.padding(0.dp),
|
||||||
|
beyondBoundsPageCount = 4,
|
||||||
userScrollEnabled = false
|
userScrollEnabled = false
|
||||||
) { page ->
|
) { page ->
|
||||||
when (page) {
|
when (page) {
|
||||||
0 -> Agent()
|
0 -> Agent()
|
||||||
1 -> Home(isPageVisible = pagerState.currentPage == 1)
|
1 -> Home()
|
||||||
2 -> Add()
|
2 -> Add()
|
||||||
3 -> Notifications()
|
3 -> Notifications()
|
||||||
4 -> Profile()
|
4 -> Profile()
|
||||||
@@ -330,9 +333,7 @@ fun IndexScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Home(
|
fun Home() {
|
||||||
isPageVisible: Boolean = true
|
|
||||||
) {
|
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -348,7 +349,7 @@ fun Home(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
MomentsList(isPageVisible = isPageVisible)
|
MomentsList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +423,13 @@ fun Profile() {
|
|||||||
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
|
systemUiController.setStatusBarColor(Color.Transparent, !AppState.darkMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面退出时清理个人资料相关资源
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
ResourceCleanupManager.cleanupPageResources("profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
@@ -511,29 +519,14 @@ fun SideMenuContent(
|
|||||||
var messageNotificationEnabled by remember { mutableStateOf(true) }
|
var messageNotificationEnabled by remember { mutableStateOf(true) }
|
||||||
var darkModeEnabled by remember { mutableStateOf(AppState.darkMode) }
|
var darkModeEnabled by remember { mutableStateOf(AppState.darkMode) }
|
||||||
|
|
||||||
// 同步暗色模式状态
|
// 菜单背景色 #FAF9FB
|
||||||
LaunchedEffect(AppState.darkMode) {
|
val menuBackgroundColor = Color(0xFFFAF9FB)
|
||||||
darkModeEnabled = AppState.darkMode
|
// 遮罩颜色 黑色透明度0.6
|
||||||
}
|
val overlayColor = Color.Black.copy(alpha = 0.6f)
|
||||||
|
// 卡片背景色 白色
|
||||||
// 菜单背景色 - 根据暗色模式适配
|
val cardBackgroundColor = Color.White
|
||||||
val menuBackgroundColor = if (darkModeEnabled) {
|
// 跟随系统文字颜色 #979499
|
||||||
appColors.secondaryBackground // 暗色模式:深灰色
|
val followSystemTextColor = Color(0xFF979499)
|
||||||
} else {
|
|
||||||
Color(0xFFFAF9FB) // 亮色模式:浅灰色
|
|
||||||
}
|
|
||||||
// 卡片背景色 - 根据暗色模式适配
|
|
||||||
val cardBackgroundColor = if (darkModeEnabled) {
|
|
||||||
appColors.background // 暗色模式:深色背景
|
|
||||||
} else {
|
|
||||||
Color.White // 亮色模式:白色
|
|
||||||
}
|
|
||||||
// 文字颜色 - 根据暗色模式适配
|
|
||||||
val textColor = appColors.text
|
|
||||||
// 图标颜色 - 根据暗色模式适配
|
|
||||||
val iconColor = appColors.text
|
|
||||||
// 跟随系统文字颜色 - 根据暗色模式适配
|
|
||||||
val followSystemTextColor = appColors.secondaryText
|
|
||||||
// 开关开启颜色 #7C45ED
|
// 开关开启颜色 #7C45ED
|
||||||
val switchActiveColor = Color(0xFF7C45ED)
|
val switchActiveColor = Color(0xFF7C45ED)
|
||||||
|
|
||||||
@@ -541,6 +534,24 @@ fun SideMenuContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -552,33 +563,34 @@ fun SideMenuContent(
|
|||||||
// 顶部状态栏间距
|
// 顶部状态栏间距
|
||||||
val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
|
||||||
|
|
||||||
// 扫一扫功能入口
|
// 扫一扫功能入口 - 右边距离右边66pt
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.offset(x = (-60).dp, y = 88.dp)
|
.offset(x = (-112).dp, y = 88.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
coroutineScope.launch {
|
// TODO: 实现扫一扫功能
|
||||||
onClose()
|
|
||||||
navController.navigate(NavigationRoute.ScanQr.route)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// 扫一扫图标(使用现有图标或占位)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.sao),
|
painter = painterResource(id = R.mipmap.sao),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
colorFilter = ColorFilter.tint(iconColor)
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
// 绝对定位的"扫一扫"文字:上方71.5dp,右侧66dp
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.scan_qr),
|
text = stringResource(R.string.scan_qr),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor,
|
color = Color.Black,
|
||||||
textAlign = TextAlign.Center
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = (-66).dp, y = 91.5.dp)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// QR码图标 - 右边距离右边112dp,上边距离上边68pt
|
// QR码图标 - 右边距离右边112dp,上边距离上边68pt
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.qr_code_icon),
|
painter = painterResource(id = R.mipmap.qr_code_icon),
|
||||||
@@ -590,7 +602,7 @@ fun SideMenuContent(
|
|||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
// TODO: 实现QR码功能
|
// TODO: 实现QR码功能
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(iconColor)
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt(绝对定位)
|
// 菜单选项卡片组 - 第一组卡片上方距离上方108pt(绝对定位)
|
||||||
@@ -604,8 +616,6 @@ fun SideMenuContent(
|
|||||||
// 第一组卡片:编辑资料、账号安全、收藏
|
// 第一组卡片:编辑资料、账号安全、收藏
|
||||||
MenuCard(
|
MenuCard(
|
||||||
backgroundColor = cardBackgroundColor,
|
backgroundColor = cardBackgroundColor,
|
||||||
textColor = textColor,
|
|
||||||
iconColor = iconColor,
|
|
||||||
width = 270.dp,
|
width = 270.dp,
|
||||||
height = 164.dp,
|
height = 164.dp,
|
||||||
items = listOf(
|
items = listOf(
|
||||||
@@ -645,8 +655,6 @@ fun SideMenuContent(
|
|||||||
// 第二组卡片:暗色模式、消息通知
|
// 第二组卡片:暗色模式、消息通知
|
||||||
MenuCard(
|
MenuCard(
|
||||||
backgroundColor = cardBackgroundColor,
|
backgroundColor = cardBackgroundColor,
|
||||||
textColor = textColor,
|
|
||||||
iconColor = iconColor,
|
|
||||||
width = 270.dp,
|
width = 270.dp,
|
||||||
height = 112.dp, // 根据设计图,第二组卡片高度为112dp
|
height = 112.dp, // 根据设计图,第二组卡片高度为112dp
|
||||||
items = listOf(
|
items = listOf(
|
||||||
@@ -701,8 +709,6 @@ fun SideMenuContent(
|
|||||||
// 第三组卡片:关于派派、反馈、退出登录
|
// 第三组卡片:关于派派、反馈、退出登录
|
||||||
MenuCard(
|
MenuCard(
|
||||||
backgroundColor = cardBackgroundColor,
|
backgroundColor = cardBackgroundColor,
|
||||||
textColor = textColor,
|
|
||||||
iconColor = iconColor,
|
|
||||||
width = 270.dp,
|
width = 270.dp,
|
||||||
height = 164.dp,
|
height = 164.dp,
|
||||||
items = listOf(
|
items = listOf(
|
||||||
@@ -770,8 +776,6 @@ data class MenuItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MenuCard(
|
fun MenuCard(
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
textColor: Color,
|
|
||||||
iconColor: Color,
|
|
||||||
items: List<MenuItem>,
|
items: List<MenuItem>,
|
||||||
width: androidx.compose.ui.unit.Dp? = null,
|
width: androidx.compose.ui.unit.Dp? = null,
|
||||||
height: androidx.compose.ui.unit.Dp? = null
|
height: androidx.compose.ui.unit.Dp? = null
|
||||||
@@ -790,15 +794,14 @@ fun MenuCard(
|
|||||||
.then(if (height != null) Modifier.weight(1f) else Modifier),
|
.then(if (height != null) Modifier.weight(1f) else Modifier),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
MenuItemRow(item = item, compact = height != null, textColor = textColor, iconColor = iconColor) // 传递颜色参数
|
MenuItemRow(item = item, compact = height != null) // 传递compact参数
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, iconColor: Color) {
|
fun MenuItemRow(item: MenuItem, compact: Boolean = false) {
|
||||||
val appColors = LocalAppTheme.current
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -822,12 +825,12 @@ fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, icon
|
|||||||
painter = painterResource(id = item.icon),
|
painter = painterResource(id = item.icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
colorFilter = ColorFilter.tint(iconColor)
|
colorFilter = ColorFilter.tint(Color.Black)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = item.label,
|
text = item.label,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = textColor
|
color = Color.Black
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +841,7 @@ fun MenuItemRow(item: MenuItem, compact: Boolean = false, textColor: Color, icon
|
|||||||
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
painter = painterResource(id = R.drawable.rave_now_nav_right),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
colorFilter = ColorFilter.tint(appColors.text)
|
colorFilter = ColorFilter.tint(Color(0xFF111213))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,9 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.aiosman.ravenow.AppStore
|
import com.aiosman.ravenow.AppStore
|
||||||
import com.aiosman.ravenow.GuestLoginCheckOut
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
@@ -87,14 +86,6 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import androidx.compose.foundation.lazy.grid.items as gridItems
|
import androidx.compose.foundation.lazy.grid.items as gridItems
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
|
||||||
|
|
||||||
// 检测是否接近列表底部的扩展函数
|
// 检测是否接近列表底部的扩展函数
|
||||||
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
|
fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean {
|
||||||
@@ -118,7 +109,7 @@ fun Agent() {
|
|||||||
var pagerState = rememberPagerState { tabCount }
|
var pagerState = rememberPagerState { tabCount }
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val viewModel: AgentViewModel = AgentViewModel
|
val viewModel: AgentViewModel = viewModel()
|
||||||
|
|
||||||
// 确保推荐Agent数据已加载
|
// 确保推荐Agent数据已加载
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -280,7 +271,6 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.chatRooms.isNotEmpty()) {
|
|
||||||
// 热门聊天室
|
// 热门聊天室
|
||||||
stickyHeader(key = "hot_rooms_header") {
|
stickyHeader(key = "hot_rooms_header") {
|
||||||
Row(
|
Row(
|
||||||
@@ -326,10 +316,7 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 只有当热门聊天室有数据时,才展示“发现更多”区域
|
|
||||||
if (viewModel.chatRooms.isNotEmpty()) {
|
|
||||||
item { Spacer(modifier = Modifier.height(20.dp)) }
|
item { Spacer(modifier = Modifier.height(20.dp)) }
|
||||||
|
|
||||||
// "发现更多" 标题 - 吸顶
|
// "发现更多" 标题 - 吸顶
|
||||||
@@ -386,21 +373,26 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多指示器(仅在展示"发现更多"时显示)
|
// 加载更多指示器
|
||||||
if (viewModel.isLoadingMore) {
|
if (viewModel.isLoadingMore) {
|
||||||
item {
|
item {
|
||||||
Box(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 24.dp),
|
.padding(vertical = 24.dp),
|
||||||
contentAlignment = Alignment.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
androidx.compose.material3.CircularProgressIndicator(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
modifier = Modifier.size(24.dp),
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.text,
|
||||||
modifier = Modifier.size(80.dp)
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "加载中...",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,7 +459,8 @@ fun AgentCardSquare(
|
|||||||
navController: NavHostController
|
navController: NavHostController
|
||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val cardHeight = 210.dp
|
val cardHeight = 180.dp
|
||||||
|
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
@@ -476,76 +469,96 @@ fun AgentCardSquare(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(cardHeight)
|
.height(cardHeight)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
.noRippleClickable {
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
}) {
|
}) {
|
||||||
lastClickTime = System.currentTimeMillis()
|
lastClickTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
// 背景大图
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = agentItem.avatar,
|
|
||||||
contentDescription = agentItem.title,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
|
||||||
defaultRes = R.mipmap.rider_pro_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
// 底部渐变与文字
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.offset(y = 4.dp)
|
||||||
.fillMaxWidth()
|
.size(avatarSize)
|
||||||
.background(
|
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
|
||||||
Brush.verticalGradient(
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
0f to Color.Transparent,
|
contentAlignment = Alignment.Center
|
||||||
1f to Color(0xB2000000)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.padding(12.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.group_copy),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(avatarSize),
|
||||||
|
)
|
||||||
|
if (agentItem.avatar.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = agentItem.avatar,
|
||||||
|
contentDescription = "Agent头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(avatarSize)
|
||||||
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容区域(名称和描述)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 40.dp) // 为底部聊天按钮预留空间
|
.padding(top = 4.dp + avatarSize + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = agentItem.title,
|
text = agentItem.title,
|
||||||
color = Color.White,
|
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
|
color = AppColors.text,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = agentItem.desc,
|
text = agentItem.desc,
|
||||||
color = Color.White.copy(alpha = 0.92f),
|
fontSize = 12.sp,
|
||||||
fontSize = 11.sp,
|
color = AppColors.secondaryText,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 底部居中 Chat 按钮
|
// 聊天按钮
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(bottom = 12.dp)
|
.padding(bottom = 12.dp)
|
||||||
.width(70.dp)
|
.width(60.dp)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.background(AppColors.text, RoundedCornerShape(16.dp))
|
.background(
|
||||||
.noRippleClickable {
|
color = AppColors.text,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
topStart = 14.dp,
|
||||||
|
topEnd = 14.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 14.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
// 检查游客模式,如果是游客则跳转登录
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
} else {
|
} else {
|
||||||
viewModel.createSingleChat(agentItem.openId)
|
viewModel.createSingleChat(agentItem.openId)
|
||||||
viewModel.goToChatAi(agentItem.openId, navController)
|
viewModel.goToChatAi(
|
||||||
|
agentItem.openId,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
lastClickTime = System.currentTimeMillis()
|
lastClickTime = System.currentTimeMillis()
|
||||||
@@ -555,9 +568,9 @@ fun AgentCardSquare(
|
|||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
text = stringResource(R.string.chat),
|
text = stringResource(R.string.chat),
|
||||||
|
fontSize = 15.sp,
|
||||||
color = AppColors.background,
|
color = AppColors.background,
|
||||||
fontSize = 13.sp,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,149 +579,74 @@ fun AgentCardSquare(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
if (agentItems.isEmpty()) return
|
|
||||||
|
|
||||||
val pagerState = rememberPagerState(pageCount = { agentItems.size })
|
// 每页显示5个agent
|
||||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
val itemsPerPage = 5
|
||||||
val cardAspect = 1133.5f / 846.4f
|
val totalPages = (agentItems.size + itemsPerPage - 1) / itemsPerPage
|
||||||
// 外层 LazyColumn 左右各 8dp + Pager contentPadding 左右各 20dp
|
|
||||||
val horizontalPaddings = 56.dp
|
if (totalPages > 0) {
|
||||||
val pagerHeight = (screenWidth - horizontalPaddings) * cardAspect
|
val pagerState = rememberPagerState(pageCount = { totalPages })
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
// Agent内容
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(pagerHeight)
|
.height(310.dp)
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 20.dp),
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 4.dp),
|
||||||
pageSpacing = 12.dp
|
pageSpacing = 0.dp
|
||||||
) { page ->
|
) { page ->
|
||||||
// 缩放效果
|
// 计算当前页面的偏移量
|
||||||
val pageOffset = (
|
val pageOffset = (
|
||||||
(pagerState.currentPage - page) + pagerState
|
(pagerState.currentPage - page) + pagerState
|
||||||
.currentPageOffsetFraction
|
.currentPageOffsetFraction
|
||||||
).coerceIn(-1f, 1f)
|
).coerceIn(-1f, 1f)
|
||||||
val scale = 1f - (0.06f * kotlin.math.abs(pageOffset))
|
|
||||||
|
|
||||||
AgentLargeCard(
|
// 根据偏移量计算缩放比例
|
||||||
agentItem = agentItems[page],
|
val scale = 1f - (0.1f * kotlin.math.abs(pageOffset))
|
||||||
|
|
||||||
|
AgentPage(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
navController = LocalNavController.current,
|
agentItems = agentItems.drop(page * itemsPerPage).take(itemsPerPage),
|
||||||
|
page = page,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.height(310.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SuspiciousIndentation")
|
|
||||||
@Composable
|
|
||||||
fun AgentLargeCard(
|
|
||||||
agentItem: AgentItem,
|
|
||||||
viewModel: AgentViewModel,
|
|
||||||
navController: NavHostController,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val AppColors = LocalAppTheme.current
|
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(846.4f / 1133.5f)
|
|
||||||
.clip(RoundedCornerShape(24.dp))
|
|
||||||
.noRippleClickable {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// 背景大图
|
|
||||||
CustomAsyncImage(
|
|
||||||
imageUrl = agentItem.avatar,
|
|
||||||
contentDescription = agentItem.title,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
|
||||||
defaultRes = R.mipmap.rider_pro_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
// 底部渐变与文字
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
0f to Color.Transparent,
|
|
||||||
1f to Color(0xB2000000)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 56.dp) // 为底部聊天按钮预留空间
|
|
||||||
) {
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = agentItem.title,
|
|
||||||
color = Color.White,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = agentItem.desc,
|
|
||||||
color = Color.White.copy(alpha = 0.92f),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部居中 Chat 按钮
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.widthIn(min = 180.dp)
|
|
||||||
.fillMaxWidth(0.65f)
|
|
||||||
.height(44.dp)
|
|
||||||
.background(AppColors.text, RoundedCornerShape(22.dp))
|
|
||||||
.noRippleClickable {
|
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
|
||||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
|
|
||||||
navController.navigate(NavigationRoute.Login.route)
|
|
||||||
} else {
|
|
||||||
viewModel.createSingleChat(agentItem.openId)
|
|
||||||
viewModel.goToChatAi(agentItem.openId, navController)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
lastClickTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
navController = LocalNavController.current,
|
||||||
) {
|
|
||||||
androidx.compose.material3.Text(
|
|
||||||
text = stringResource(R.string.chat),
|
|
||||||
color = AppColors.background,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 指示器
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(30.dp)
|
||||||
|
.padding(top = 12.dp),
|
||||||
|
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Center
|
||||||
|
) {
|
||||||
|
repeat(totalPages) { index ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.size(3.dp)
|
||||||
|
.background(
|
||||||
|
color = if (pagerState.currentPage == index) AppColors.text else AppColors.secondaryText.copy(
|
||||||
|
alpha = 0.3f
|
||||||
|
),
|
||||||
|
shape = androidx.compose.foundation.shape.CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -756,16 +694,23 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.group_copy),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (agentItem.avatar.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = agentItem.avatar,
|
imageUrl = agentItem.avatar,
|
||||||
contentDescription = "Agent头像",
|
contentDescription = "Agent头像",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(RoundedCornerShape(24.dp)),
|
.clip(RoundedCornerShape(24.dp)),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
defaultRes = R.mipmap.group_copy
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
@@ -899,7 +844,7 @@ fun ChatRoomCard(
|
|||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val cardSize = 180.dp
|
val cardSize = 180.dp
|
||||||
val viewModel: AgentViewModel = AgentViewModel
|
val viewModel: AgentViewModel = viewModel()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
@@ -918,16 +863,26 @@ fun ChatRoomCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(120.dp)
|
.size(120.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color.Transparent,
|
color = AppColors.background,
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
Column(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
iterations = LottieConstants.IterateForever,
|
verticalArrangement = Arrangement.Center
|
||||||
modifier = Modifier.size(96.dp)
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
color = AppColors.main
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "加入中...",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -961,6 +916,7 @@ fun ChatRoomCard(
|
|||||||
// 优先显示banner,如果没有banner则显示头像
|
// 优先显示banner,如果没有banner则显示头像
|
||||||
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
||||||
|
|
||||||
|
if (imageUrl.isNotEmpty()) {
|
||||||
CustomAsyncImage(
|
CustomAsyncImage(
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
||||||
@@ -972,9 +928,17 @@ fun ChatRoomCard(
|
|||||||
topEnd = 12.dp,
|
topEnd = 12.dp,
|
||||||
bottomStart = 0.dp,
|
bottomStart = 0.dp,
|
||||||
bottomEnd = 0.dp)),
|
bottomEnd = 0.dp)),
|
||||||
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
defaultRes = R.mipmap.rider_pro_agent
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// 默认房间图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent),
|
||||||
|
contentDescription = "默认房间图标",
|
||||||
|
modifier = Modifier.size(cardSize * 0.4f),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 房间名称,重叠在底部
|
// 房间名称,重叠在底部
|
||||||
Box(
|
Box(
|
||||||
@@ -1017,13 +981,9 @@ fun ChatRoomCard(
|
|||||||
Text(
|
Text(
|
||||||
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
|
text = "${chatRoom.memberCount} ${stringResource(R.string.chatting_now)}",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier
|
modifier = Modifier.alpha(0.6f),
|
||||||
.alpha(0.6f)
|
|
||||||
.weight(1f),
|
|
||||||
color = AppColors.text,
|
color = AppColors.text,
|
||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.W500,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
|
|||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
isRecommended = 1,
|
isRecommended = 1,
|
||||||
random = "1"
|
random = 1
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val allRooms = response.body()?.list ?: emptyList()
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ object HotAgentViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
// 预加载头像图片到缓存
|
// 预加载头像图片到缓存
|
||||||
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
|
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
|
||||||
coil3.request.ImageRequest.Builder(context)
|
coil.request.ImageRequest.Builder(context)
|
||||||
.data(agent.avatar)
|
.data(agent.avatar)
|
||||||
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
|
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
|
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
preloadedImageIds.add(agent.id)
|
preloadedImageIds.add(agent.id)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ fun NotificationsScreen() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.noRippleClickable {
|
.noRippleClickable {
|
||||||
navController.navigate(NavigationRoute.Search.route)
|
// TODO: 实现搜索功能
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import io.openim.android.sdk.models.ConversationInfo
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import com.aiosman.ravenow.data.repo.TrtcUserTypeRepository
|
|
||||||
|
|
||||||
data class Conversation(
|
data class Conversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -77,13 +76,6 @@ object MessageListViewModel : ViewModel() {
|
|||||||
// noticeInfo = info
|
// noticeInfo = info
|
||||||
//
|
//
|
||||||
// isLoading = false
|
// isLoading = false
|
||||||
if (loadChat && com.aiosman.ravenow.AppState.enableChat) {
|
|
||||||
// 预热 trtcId -> isAI 缓存,避免首次进入 Agent/Friends 列表时阻塞
|
|
||||||
try {
|
|
||||||
prewarmChatTypes(context)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,23 +163,5 @@ object MessageListViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun prewarmChatTypes(context: Context) {
|
|
||||||
val result = suspendCoroutine { continuation ->
|
|
||||||
OpenIMClient.getInstance().conversationManager.getAllConversationList(
|
|
||||||
object : OnBase<List<ConversationInfo>> {
|
|
||||||
override fun onSuccess(data: List<ConversationInfo>?) {
|
|
||||||
continuation.resumeWith(Result.success(data ?: emptyList()))
|
|
||||||
}
|
|
||||||
override fun onError(code: Int, error: String?) {
|
|
||||||
continuation.resumeWith(Result.failure(Exception("Error $code: $error")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val trtcIds = result.filter { it.conversationType == 1 }
|
|
||||||
.mapNotNull { it.userID }
|
|
||||||
.distinct()
|
|
||||||
TrtcUserTypeRepository.ensureTypes(context, trtcIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,6 @@ 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.offset
|
|
||||||
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
|
||||||
@@ -35,7 +34,6 @@ 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
|
||||||
@@ -49,12 +47,6 @@ 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 com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
|
||||||
import com.airbnb.lottie.compose.LottieAnimation
|
|
||||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
|
||||||
import com.airbnb.lottie.compose.LottieConstants
|
|
||||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智能体聊天列表页面
|
* 智能体聊天列表页面
|
||||||
@@ -91,30 +83,69 @@ fun AgentChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
|
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// 空状态
|
// 空状态
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
} else {
|
Image(
|
||||||
NetworkErrorContentCompact(
|
painter = painterResource(
|
||||||
onReload = {
|
id = if(AppState.darkMode) R.mipmap.juhao_dark
|
||||||
|
else R.mipmap.invalid_name_5),
|
||||||
|
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.agent_chat_empty_title),
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.agent_chat_empty_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
AgentChatListViewModel.refreshPager(context = context)
|
AgentChatListViewModel.refreshPager(context = context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -149,10 +180,9 @@ fun AgentChatListScreen() {
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
LottieAnimation(
|
CircularProgressIndicator(
|
||||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
modifier = Modifier.size(24.dp),
|
||||||
iterations = LottieConstants.IterateForever,
|
color = AppColors.main
|
||||||
modifier = Modifier.size(80.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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
|
||||||
@@ -29,7 +28,6 @@ import io.openim.android.sdk.models.Message
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import com.aiosman.ravenow.utils.MessageParser
|
import com.aiosman.ravenow.utils.MessageParser
|
||||||
import com.aiosman.ravenow.data.repo.TrtcUserTypeRepository
|
|
||||||
|
|
||||||
data class AgentConversation(
|
data class AgentConversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -55,7 +53,7 @@ data class AgentConversation(
|
|||||||
nickname = conversation.showName ?: "",
|
nickname = conversation.showName ?: "",
|
||||||
lastMessage = displayText, // 使用解析后的显示文本
|
lastMessage = displayText, // 使用解析后的显示文本
|
||||||
lastMessageTime = lastMessage.time.formatChatTime(context),
|
lastMessageTime = lastMessage.time.formatChatTime(context),
|
||||||
avatar = "${ApiClient.BASE_SERVER}${conversation.faceURL}",
|
avatar = "${ApiClient.BASE_API_URL+"/"}${conversation.faceURL}"+"?token="+"${AppStore.token}".replace("storage/avatars/", "/avatar/"),
|
||||||
unreadCount = conversation.unreadCount,
|
unreadCount = conversation.unreadCount,
|
||||||
trtcUserId = conversation.userID ?: "",
|
trtcUserId = conversation.userID ?: "",
|
||||||
displayText = displayText,
|
displayText = displayText,
|
||||||
@@ -129,7 +127,15 @@ object AgentChatListViewModel : ViewModel() {
|
|||||||
OpenIMClient.getInstance().conversationManager.getAllConversationList(
|
OpenIMClient.getInstance().conversationManager.getAllConversationList(
|
||||||
object : OnBase<List<ConversationInfo>> {
|
object : OnBase<List<ConversationInfo>> {
|
||||||
override fun onSuccess(data: List<ConversationInfo>?) {
|
override fun onSuccess(data: List<ConversationInfo>?) {
|
||||||
continuation.resumeWith(Result.success(data ?: emptyList()))
|
// 过滤出智能体会话(单聊类型,且可能有特定标识)
|
||||||
|
val agentConversations = data?.filter { conversation ->
|
||||||
|
// 这里需要根据实际业务逻辑来过滤智能体会话
|
||||||
|
// 可能通过会话类型、用户ID前缀、或其他标识来判断
|
||||||
|
conversation.conversationType == 1 // 1 表示单聊
|
||||||
|
// 可以添加更多过滤条件,比如:
|
||||||
|
// && conversation.userID?.startsWith("ai_") == true
|
||||||
|
} ?: emptyList()
|
||||||
|
continuation.resumeWith(Result.success(agentConversations))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(code: Int, error: String?) {
|
override fun onError(code: Int, error: String?) {
|
||||||
@@ -139,22 +145,7 @@ object AgentChatListViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅单聊
|
agentChatList = result.map { conversation ->
|
||||||
val singleChats = result.filter { it.conversationType == 1 }
|
|
||||||
val trtcIds = singleChats.mapNotNull { it.userID }.distinct()
|
|
||||||
// 预热缓存(包含AI)
|
|
||||||
try {
|
|
||||||
TrtcUserTypeRepository.ensureTypes(context, trtcIds)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w("AgentChatListViewModel", "ensureTypes failed: ${e.message}")
|
|
||||||
}
|
|
||||||
// 过滤出 AI 会话
|
|
||||||
val filtered = singleChats.filter { conv ->
|
|
||||||
val id = conv.userID ?: return@filter false
|
|
||||||
TrtcUserTypeRepository.getCachedType(id) == true
|
|
||||||
}
|
|
||||||
|
|
||||||
agentChatList = filtered.map { conversation ->
|
|
||||||
AgentConversation.convertToAgentConversation(conversation, context)
|
AgentConversation.convertToAgentConversation(conversation, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,9 @@ 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 com.aiosman.ravenow.utils.NetworkUtils
|
import com.aiosman.ravenow.utils.NetworkUtils
|
||||||
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
|
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
|
|
||||||
|
|
||||||
data class CombinedConversation(
|
data class CombinedConversation(
|
||||||
val type: String, // "agent", "group", or "friend"
|
val type: String, // "agent", "group", or "friend"
|
||||||
@@ -123,88 +121,35 @@ fun AllChatListScreen() {
|
|||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
// 监听各个 ViewModel 的列表变化
|
|
||||||
val agentChatList = AgentChatListViewModel.agentChatList
|
|
||||||
val groupChatList = GroupChatListViewModel.groupChatList
|
|
||||||
val friendChatList = FriendChatListViewModel.friendChatList
|
|
||||||
|
|
||||||
// 当列表变化时,自动更新合并列表
|
|
||||||
LaunchedEffect(agentChatList, groupChatList, friendChatList) {
|
|
||||||
val combinedList = mutableListOf<CombinedConversation>()
|
|
||||||
|
|
||||||
agentChatList.forEach { agent ->
|
|
||||||
combinedList.add(CombinedConversation(type = "agent", agentConversation = agent))
|
|
||||||
}
|
|
||||||
|
|
||||||
groupChatList.forEach { group ->
|
|
||||||
combinedList.add(CombinedConversation(type = "group", groupConversation = group))
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
allConversations = sortedList
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听加载状态
|
|
||||||
val isAnyLoading = AgentChatListViewModel.isLoading ||
|
|
||||||
GroupChatListViewModel.isLoading ||
|
|
||||||
FriendChatListViewModel.isLoading
|
|
||||||
|
|
||||||
// 当加载状态变化时,更新 isLoading
|
|
||||||
LaunchedEffect(isAnyLoading) {
|
|
||||||
if (isAnyLoading) {
|
|
||||||
// 如果有任何数据正在加载,确保显示加载状态
|
|
||||||
if (!isLoading) {
|
|
||||||
isLoading = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 所有数据加载完成
|
|
||||||
if (isLoading) {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = rememberPullRefreshState(
|
val state = rememberPullRefreshState(
|
||||||
refreshing = refreshing,
|
refreshing = refreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
refreshing = true
|
refreshing = true
|
||||||
// 刷新所有类型的数据
|
refreshAllData(context,
|
||||||
AgentChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
onSuccess = { conversations ->
|
||||||
GroupChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
allConversations = conversations
|
||||||
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
|
refreshing = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
refreshing = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听刷新状态
|
|
||||||
LaunchedEffect(AgentChatListViewModel.refreshing, GroupChatListViewModel.refreshing, FriendChatListViewModel.refreshing) {
|
|
||||||
val isAnyRefreshing = AgentChatListViewModel.refreshing ||
|
|
||||||
GroupChatListViewModel.refreshing ||
|
|
||||||
FriendChatListViewModel.refreshing
|
|
||||||
if (!isAnyRefreshing && refreshing) {
|
|
||||||
refreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
// 初始化加载所有类型的数据
|
refreshAllData(context,
|
||||||
AgentChatListViewModel.refreshPager(context = context)
|
onSuccess = { conversations ->
|
||||||
GroupChatListViewModel.refreshPager(context = context)
|
allConversations = conversations
|
||||||
FriendChatListViewModel.refreshPager(context = context)
|
isLoading = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -218,31 +163,74 @@ fun AllChatListScreen() {
|
|||||||
.pullRefresh(state)
|
.pullRefresh(state)
|
||||||
) {
|
) {
|
||||||
if (allConversations.isEmpty() && !isLoading) {
|
if (allConversations.isEmpty() && !isLoading) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.offset(y = (-40).dp)
|
|
||||||
) {
|
) {
|
||||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
|
||||||
if (isNetworkAvailable) {
|
if (isNetworkAvailable) {
|
||||||
ChatEmptyStateView()
|
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
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_empty_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
NetworkErrorContentCompact(
|
Spacer(modifier = Modifier.height(39.dp))
|
||||||
onReload = {
|
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.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
// 重新加载所有类型的数据
|
refreshAllData(context,
|
||||||
AgentChatListViewModel.refreshPager(context = context)
|
onSuccess = { conversations ->
|
||||||
GroupChatListViewModel.refreshPager(context = context)
|
allConversations = conversations
|
||||||
FriendChatListViewModel.refreshPager(context = context)
|
isLoading = false
|
||||||
|
},
|
||||||
|
onError = { errorMsg ->
|
||||||
|
error = errorMsg
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -359,3 +347,44 @@ fun AllChatListScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user