4 Commits

Author SHA1 Message Date
a1196715d0 Feat: Add News tab and related functionality
- Added a "News" tab to the main index screen.
- Implemented API parameters for fetching news-specific posts: `imageTag`, `search`, `advancedSearch`, `newsFilter`, `onlyNews`, `newsSource`, `newsLanguage`, `newsCategory`, `requireImageCache`.
- Updated `Moment` data class and `MomentEntity` to include news-related fields like `isNews`, `newsTitle`, `newsUrl`, etc.
- Created `News.kt` composable and `NewsViewModel.kt` to display and manage news items.
- Updated `MomentLoader` to include a `newsOnly` parameter for fetching only news items.
- Added Japanese translations for new index tab strings: "Worldwide", "Dynamic", "Following", "Hot", and "News".
- Adjusted tab count and layout based on guest/logged-in user status to accommodate the new "News" tab.
2025-09-15 18:31:24 +08:00
68273ae166 Fix: Correct BASE_SERVER URL for debug and release builds
The BASE_SERVER constant was incorrectly assigning the release URL to debug builds and vice-versa. This commit fixes the logic to ensure the correct API endpoint is used for each build type.
2025-09-15 13:46:01 +08:00
6c7be4ba47 feat: Add string resources for Create Agent V2
This commit introduces new string resources for the "Create Agent V2" feature.
These strings are provided in English, Chinese, and Japanese to support localization.

The added strings cover various UI elements and messages within the Create Agent V2 flow, including:
- Titles and labels (e.g., "Create AI", "Name", "Description")
- User guidance and prompts (e.g., "Hello %s! What would you like to create today?", "An AI that writes poetry...")
- Action button texts (e.g., "AI Enhancement", "Manually Create AI", "Alright, that's the one")
- Status messages (e.g., "Generating...", "Creating...", "Thinking for you")
- Accessibility descriptions for icons (e.g., "AI Avatar", "Edit Icon")
2025-09-15 13:42:58 +08:00
cf25540417 Refactor: Implement V2 of Create Agent UI and logic
- Introduced `CreateAgentV2Screen` and `CreateAgentV2ViewModel` for a new agent creation experience.
- Implemented AI-powered agent info generation based on user input, including title and description.
- Added a "manual mode" for users who prefer to input agent details directly.
- Enhanced UI with gradient borders, loading animations, and improved layout.
- Integrated avatar selection and cropping using `AgentImageCropScreen`.
- Refactored `AddAgentViewModel` to support state persistence across page navigation and to store generated input text.
- Updated API client to include a longer timeout for agent info generation requests.
- Added new drawable resources for UI elements.
- Switched `Const.BASE_SERVER` to use the release URL for debug builds.
- Replaced the old `AddAgentScreen` with the new `CreateAgentV2Screen` in navigation.
2025-09-15 12:03:39 +08:00
288 changed files with 3173 additions and 7454 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-17T06:25:35.585100400Z">
<DropdownSelection timestamp="2025-09-09T09:51:06.656104400Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.216:5555;connection=698a7727" />
<DeviceId pluginId="Default" identifier="serial=192.168.0.227:45035;connection=094cb92e" />
</handle>
</Target>
</DropdownSelection>

32
.idea/gradle.xml generated
View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -43,12 +43,7 @@ import com.google.firebase.analytics.analytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.aiosman.ravenow.ui.splash.SplashScreen
class MainActivity : ComponentActivity() {
// Firebase Analytics
@@ -127,84 +122,75 @@ class MainActivity : ComponentActivity() {
}
setContent {
var showSplash by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(2000)
showSplash = false
}
if (showSplash) {
SplashScreen()
} else {
CompositionLocalProvider(
LocalAppTheme provides AppState.appTheme
) {
CheckUpdateDialog()
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
val action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "followCount") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "TRTC_NEW_MESSAGE") {
val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it,0)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
}
CompositionLocalProvider(
LocalAppTheme provides AppState.appTheme
) {
CheckUpdateDialog()
Navigation(startDestination) { navController ->
// 处理带有 postId 的通知点击
val postId = intent.getStringExtra("POST_ID")
var commentId = intent.getStringExtra("COMMENT_ID")
val action = intent.getStringExtra("ACTION")
if (action == "newFollow") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "followCount") {
navController.navigate(NavigationRoute.Followers.route)
return@Navigation
}
if (action == "TRTC_NEW_MESSAGE") {
val userService:UserService = UserServiceImpl()
val sender = intent.getStringExtra("SENDER")
sender?.let {
scope.launch {
try {
val profile = userService.getUserProfileByTrtcUserId(it,0)
navController.navigate(NavigationRoute.Chat.route.replace(
"{id}",
profile.id.toString()
))
}catch (e:Exception){
e.printStackTrace()
}
}
return@Navigation
}
return@Navigation
}
if (commentId == null) {
commentId = "0"
}
if (commentId == null) {
commentId = "0"
}
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
if (postId != null) {
Log.d("MainActivity", "Navigation to Post$postId")
navController.navigateToPost(
id = postId.toInt(),
highlightCommentId = commentId.toInt(),
initImagePagerIndex = 0
)
}
// 处理分享过来的图片
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
val imageUris: List<Uri>? = if (intent.action == Intent.ACTION_SEND) {
listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM)!!)
} else {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
NewPostViewModel.asNewPostWithImageUris(imageUris!!.map { it.toString() })
navController.navigate(NavigationRoute.NewPost.route)
}
}
}
}
}
}
/**
* 请求通知权限
*/

View File

@@ -32,9 +32,8 @@ data class Moment(
val time: String,
@SerializedName("isFollowed")
val isFollowed: Boolean,
// 新闻相关字段
@SerializedName("isNews")
val isNews: Boolean = false,
val isNews: Boolean? = null,
@SerializedName("newsTitle")
val newsTitle: String? = null,
@SerializedName("newsUrl")
@@ -47,6 +46,14 @@ data class Moment(
val newsLanguage: String? = null,
@SerializedName("newsContent")
val newsContent: String? = null,
@SerializedName("hasFullText")
val hasFullText: Boolean? = null,
@SerializedName("summary")
val summary: String? = null,
@SerializedName("publishedAt")
val publishedAt: String? = null,
@SerializedName("imageCached")
val imageCached: Boolean? = null,
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
@@ -75,14 +82,17 @@ data class Moment(
authorId = user.id.toInt(),
liked = isLiked,
isFavorite = isFavorite,
// 新闻相关字段
isNews = isNews,
newsTitle = newsTitle ?: "",
newsUrl = newsUrl ?: "",
newsSource = newsSource ?: "",
newsCategory = newsCategory ?: "",
newsLanguage = newsLanguage ?: "",
newsContent = newsContent ?: ""
newsTitle = newsTitle,
newsUrl = newsUrl,
newsSource = newsSource,
newsCategory = newsCategory,
newsLanguage = newsLanguage,
newsContent = newsContent,
hasFullText = hasFullText,
summary = summary,
publishedAt = publishedAt,
imageCached = imageCached,
)
}
}

View File

@@ -13,9 +13,11 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
fun getSafeOkHttpClient(
authInterceptor: AuthInterceptor? = null
authInterceptor: AuthInterceptor? = null,
timeoutSeconds: Long = 30
): OkHttpClient {
return OkHttpClient.Builder()
.apply {
@@ -23,6 +25,9 @@ fun getSafeOkHttpClient(
addInterceptor(it)
}
}
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.build()
}
@@ -56,7 +61,7 @@ class AuthInterceptor() : Interceptor {
val client = Retrofit.Builder()
.baseUrl(ApiClient.RETROFIT_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getSafeOkHttpClient())
.client(getSafeOkHttpClient(timeoutSeconds = 30))
.build()
.create(RaveNowAPI::class.java)
@@ -75,7 +80,10 @@ object ApiClient {
val RETROFIT_URL = "${BASE_API_URL}/"
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
private val okHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor())
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 30)
}
private val longTimeoutOkHttpClient: OkHttpClient by lazy {
getSafeOkHttpClient(authInterceptor = AuthInterceptor(), timeoutSeconds = 120)
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
@@ -84,9 +92,19 @@ object ApiClient {
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private val longTimeoutRetrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(RETROFIT_URL)
.client(longTimeoutOkHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RaveNowAPI by lazy {
retrofit.create(RaveNowAPI::class.java)
}
val longTimeoutApi: RaveNowAPI by lazy {
longTimeoutRetrofit.create(RaveNowAPI::class.java)
}
fun formatTime(date: Date): String {
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())

View File

@@ -42,6 +42,20 @@ data class AgentMomentRequestBody(
val sessionId: String
)
data class GenerateAgentInfoRequestBody(
@SerializedName("descriptionText")
val descriptionText: String
)
data class GenerateAgentInfoResponseBody(
@SerializedName("title")
val title: String,
@SerializedName("description")
val description: String,
@SerializedName("content")
val content: String
)
data class SingleChatRequestBody(
@SerializedName("agentOpenId")
val agentOpenId: String? = null,
@@ -81,9 +95,7 @@ data class CreateGroupChatRequestBody(
data class JoinGroupChatRequestBody(
@SerializedName("trtcId")
val trtcId: String? = null,
@SerializedName("roomId")
val roomId: Int? = null,
val trtcId: String,
)
data class LoginUserRequestBody(
@@ -273,350 +285,6 @@ data class RemoveAccountRequestBody(
val password: String,
)
// API 错误响应(用于加入房间等接口的错误处理)
data class ApiErrorResponse(
@SerializedName("err")
val error: String,
@SerializedName("success")
val success: Boolean
)
// 群聊中的用户信息
data class GroupChatUser(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("userSessionId")
val userSessionId: String,
@SerializedName("sessions")
val sessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("prompts")
val prompts: Any?, // 根据实际需要可以定义具体类型
@SerializedName("isAgent")
val isAgent: Boolean
)
// 智能体角色信息
data class GroupChatPrompt(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("Title")
val title: String,
@SerializedName("Desc")
val desc: String,
@SerializedName("Value")
val value: String,
@SerializedName("Enable")
val enable: Boolean,
@SerializedName("UserSessions")
val userSessions: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Avatar")
val avatar: String,
@SerializedName("AuthorId")
val authorId: Int?,
@SerializedName("Author")
val author: Any?, // 根据实际需要可以定义具体类型
@SerializedName("TokenCount")
val tokenCount: Int,
@SerializedName("OpenId")
val openId: String,
@SerializedName("Public")
val public: Boolean,
@SerializedName("BreakMode")
val breakMode: Boolean,
@SerializedName("DocNamespace")
val docNamespace: String,
@SerializedName("UseRag")
val useRag: Boolean,
@SerializedName("RagThreshold")
val ragThreshold: Double,
@SerializedName("WorkflowId")
val workflowId: Int?,
@SerializedName("Workflow")
val workflow: Any?, // 根据实际需要可以定义具体类型
@SerializedName("WorkflowInputs")
val workflowInputs: Any?, // 根据实际需要可以定义具体类型
@SerializedName("Source")
val source: String,
@SerializedName("categories")
val categories: Any? // 根据实际需要可以定义具体类型
)
// 群聊详细信息响应
data class GroupChatResponse(
@SerializedName("ID")
val id: Int,
@SerializedName("CreatedAt")
val createdAt: String,
@SerializedName("UpdatedAt")
val updatedAt: String,
@SerializedName("DeletedAt")
val deletedAt: String?,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("creatorId")
val creatorId: Int,
@SerializedName("creator")
val creator: Any?, // 根据实际需要可以定义具体类型
@SerializedName("trtcRoomId")
val trtcRoomId: String,
@SerializedName("trtcType")
val trtcType: String,
@SerializedName("cover")
val cover: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("recommendBanner")
val recommendBanner: String,
@SerializedName("isRecommended")
val isRecommended: Boolean,
@SerializedName("allowInHot")
val allowInHot: Boolean,
@SerializedName("users")
val users: List<GroupChatUser>,
@SerializedName("prompts")
val prompts: List<GroupChatPrompt>,
@SerializedName("source")
val source: String
)
data class CategoryTranslation(
@SerializedName("name")
val name: String?,
@SerializedName("description")
val description: String?
)
data class CategoryTemplate(
@SerializedName("id")
val id: Int,
@SerializedName("name")
val name: String,
@SerializedName("description")
val description: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("parentId")
val parentId: Int?,
@SerializedName("parent")
val parent: CategoryTemplate?,
@SerializedName("children")
val children: List<CategoryTemplate>?,
@SerializedName("sort")
val sort: Int,
@SerializedName("isActive")
val isActive: Boolean,
@SerializedName("promptCount")
val promptCount: Int?,
@SerializedName("createdAt")
val createdAt: String,
@SerializedName("updatedAt")
val updatedAt: String,
@SerializedName("translations")
val translations: Map<String, CategoryTranslation>?
) {
/**
* 根据语言代码获取翻译后的名称,如果没有翻译则返回默认名称
*/
fun getLocalizedName(lang: String): String {
// 尝试获取完整的语言标记(如 "zh-CN"
val translation = translations?.get(lang)
if (translation?.name != null && translation.name.isNotEmpty()) {
return translation.name
}
// 如果没有找到,尝试语言代码的前缀(如 "zh"
val langPrefix = lang.split("-", "_").firstOrNull()
if (langPrefix != null) {
translations?.entries?.forEach { (key, value) ->
if (key.startsWith(langPrefix) && value.name != null && value.name.isNotEmpty()) {
return value.name
}
}
}
// 如果没有翻译,返回默认名称
return name
}
/**
* 根据语言代码获取翻译后的描述,如果没有翻译则返回默认描述
*/
fun getLocalizedDescription(lang: String): String {
// 尝试获取完整的语言标记(如 "zh-CN"
val translation = translations?.get(lang)
if (translation?.description != null && translation.description.isNotEmpty()) {
return translation.description
}
// 如果没有找到,尝试语言代码的前缀(如 "zh"
val langPrefix = lang.split("-", "_").firstOrNull()
if (langPrefix != null) {
translations?.entries?.forEach { (key, value) ->
if (key.startsWith(langPrefix) && value.description != null && value.description.isNotEmpty()) {
return value.description
}
}
}
// 如果没有翻译,返回默认描述
return description
}
}
data class CategoryListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<CategoryTemplate>
)
// ========== Prompt Rule 相关数据类 ==========
/**
* 创建规则请求体
* @param rule 规则内容,不能为空
* @param promptId 智能体ID与 openId 二选一promptId 优先
* @param openId 智能体的 OpenIDUUID格式与 promptId 二选一
*/
data class CreatePromptRuleRequestBody(
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
val promptId: Int? = null,
@SerializedName("openId")
val openId: String? = null
)
/**
* 修改规则请求体
* @param id 规则ID必填
* @param rule 新的规则内容,不能为空
* @param promptId 要更改关联的智能体ID可选
* @param openId 要更改关联的智能体 OpenID可选
*/
data class UpdatePromptRuleRequestBody(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("promptId")
val promptId: Int? = null,
@SerializedName("openId")
val openId: String? = null
)
/**
* 规则关联的智能体信息
* @param id 智能体ID
* @param title 智能体标题
* @param avatar 智能体头像URL
*/
data class PromptRuleAgent(
@SerializedName("id")
val id: Int,
@SerializedName("title")
val title: String,
@SerializedName("avatar")
val avatar: String
)
/**
* 规则详情
* @param id 规则ID
* @param rule 规则内容
* @param creator 创建者名称
* @param creatorType 创建者类型(如 "user"
* @param scope 作用域(如 "personal"
* @param prompt 关联的智能体信息
* @param createdAt 创建时间ISO 8601 格式)
* @param updatedAt 更新时间ISO 8601 格式)
*/
data class PromptRule(
@SerializedName("id")
val id: Int,
@SerializedName("rule")
val rule: String,
@SerializedName("creator")
val creator: String,
@SerializedName("creator_type")
val creatorType: String,
@SerializedName("scope")
val scope: String,
@SerializedName("prompt")
val prompt: PromptRuleAgent,
@SerializedName("created_at")
val createdAt: String,
@SerializedName("updated_at")
val updatedAt: String
)
/**
* 规则列表响应
* @param page 当前页码
* @param pageSize 每页数量
* @param total 总记录数
* @param list 规则列表
*/
data class PromptRuleListResponse(
@SerializedName("page")
val page: Int,
@SerializedName("pageSize")
val pageSize: Int,
@SerializedName("total")
val total: Int,
@SerializedName("list")
val list: List<PromptRule>
)
/**
* 规则配额信息
* @param promptId 智能体ID
* @param promptTitle 智能体标题
* @param baseMaxCount 基础条数限制(免费配额)
* @param purchasedCount 用户购买的额外条数
* @param totalMaxCount 总可用条数(基础+购买)
* @param currentCount 当前已创建的规则条数
* @param remainingCount 剩余可用条数
* @param usagePercent 使用百分比0-100
*/
data class PromptRuleQuota(
@SerializedName("promptId")
val promptId: Int,
@SerializedName("promptTitle")
val promptTitle: String,
@SerializedName("baseMaxCount")
val baseMaxCount: Int,
@SerializedName("purchasedCount")
val purchasedCount: Int,
@SerializedName("totalMaxCount")
val totalMaxCount: Int,
@SerializedName("currentCount")
val currentCount: Int,
@SerializedName("remainingCount")
val remainingCount: Int,
@SerializedName("usagePercent")
val usagePercent: Double
)
interface RaveNowAPI {
@GET("membership/config")
@retrofit2.http.Headers("X-Requires-Auth: true")
@@ -648,6 +316,7 @@ interface RaveNowAPI {
suspend fun getPosts(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("id") postId: Int? = null,
@Query("timelineId") timelineId: Int? = null,
@Query("authorId") authorId: Int? = null,
@Query("contentSearch") contentSearch: String? = null,
@@ -655,7 +324,15 @@ interface RaveNowAPI {
@Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null,
@Query("imageTag") imageTag: String? = null,
@Query("search") search: String? = null,
@Query("advancedSearch") advancedSearch: String? = null,
@Query("newsFilter") newsFilter: String? = null,
@Query("onlyNews") onlyNews: Boolean? = null,
@Query("newsSource") newsSource: String? = null,
@Query("newsLanguage") newsLanguage: String? = null,
@Query("newsCategory") newsCategory: String? = null,
@Query("requireImageCache") requireImageCache: Boolean? = null,
): Response<ListContainer<Moment>>
@Multipart
@@ -899,8 +576,6 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int = 20,
@Query("withWorkflow") withWorkflow: Int = 1,
@Query("authorId") authorId: Int? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("random") random: Int? = null,
): Response<DataContainer<ListContainer<Agent>>>
@GET("outside/my/prompts")
@@ -929,10 +604,7 @@ interface RaveNowAPI {
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
@GET("outside/rooms/open")
suspend fun createGroupChatAi(
@Query("trtcGroupId") trtcGroupId: String? = null,
@Query("roomId") roomId: Int? = null
): Response<DataContainer<GroupChatResponse>>
suspend fun createGroupChatAi(@Query("trtcGroupId") trtcGroupId: String): Response<DataContainer<Unit>>
@POST("outside/rooms/create-single-chat")
suspend fun createSingleChat(@Body body: SingleChatRequestBody): Response<DataContainer<Unit>>
@@ -947,7 +619,6 @@ interface RaveNowAPI {
suspend fun getRooms(@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("isRecommended") isRecommended: Int = 1,
@Query("random") random: Int? = null,
): Response<ListContainer<Room>>
@GET("outside/rooms/detail")
@@ -958,240 +629,8 @@ interface RaveNowAPI {
suspend fun joinRoom(@Body body: JoinGroupChatRequestBody,
): Response<DataContainer<Room>>
@GET("outside/categories")
suspend fun getCategories(
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null,
@Query("parentId") parentId: Int? = null,
@Query("isActive") isActive: Boolean? = null,
@Query("name") name: String? = null,
@Query("withChildren") withChildren: Boolean? = null,
@Query("withParent") withParent: Boolean? = null,
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null,
@Query("lang") lang: String? = null
): Response<CategoryListResponse>
@GET("outside/categories/tree")
suspend fun getCategoryTree(
@Query("withCount") withCount: Boolean? = null,
@Query("hideEmpty") hideEmpty: Boolean? = null
): Response<DataContainer<List<CategoryTemplate>>>
@GET("outside/categories/{id}")
suspend fun getCategoryById(
@Path("id") id: Int
): Response<DataContainer<CategoryTemplate>>
@GET("outside/prompts")
suspend fun getPromptsByCategory(
@Query("categoryIds") categoryIds: List<Int>? = null,
@Query("categoryName") categoryName: String? = null,
@Query("uncategorized") uncategorized: String? = null,
@Query("page") page: Int? = null,
@Query("pageSize") pageSize: Int? = null
): Response<ListContainer<Agent>>
// ========== Prompt Rule API ==========
/**
* 创建智能体规则
*
* 功能说明:
* - 为指定的智能体创建一条新规则
* - 规则必须关联到存在的智能体(通过 promptId 或 openId 指定)
* - 创建前会自动检查配额限制,如果超出会尝试自动扩容
* - 只有创建者可以修改和删除该规则
*
* @param body 创建规则请求体
* - rule: 规则内容,不能为空
* - promptId: 智能体ID与 openId 二选一promptId 优先)
* - openId: 智能体的 OpenIDUUID格式与 promptId 二选一)
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 示例:
* ```kotlin
* val request = CreatePromptRuleRequestBody(
* rule = "禁止讨论政治话题",
* promptId = 123
* )
* val response = api.createPromptRule(request)
* ```
*/
@POST("outside/prompt/rule")
suspend fun createPromptRule(
@Body body: CreatePromptRuleRequestBody
): Response<Unit>
/**
* 修改智能体规则
*
* 功能说明:
* - 修改已存在的规则内容或关联的智能体
* - 只有规则的创建者可以修改
* - 可以同时修改规则内容和关联的智能体
* - 修改关联智能体时会重新验证配额限制
*
* @param body 修改规则请求体
* - id: 规则ID必填
* - rule: 新的规则内容,不能为空
* - promptId: 要更改关联的智能体ID可选
* - openId: 要更改关联的智能体 OpenID可选
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者creator_type 为 "user" 且 create_id 匹配)
*
* 示例:
* ```kotlin
* val request = UpdatePromptRuleRequestBody(
* id = 456,
* rule = "禁止讨论政治和敏感话题"
* )
* val response = api.updatePromptRule(request)
* ```
*/
@retrofit2.http.PUT("outside/prompt/rule")
suspend fun updatePromptRule(
@Body body: UpdatePromptRuleRequestBody
): Response<Unit>
/**
* 删除智能体规则
*
* 功能说明:
* - 删除指定的规则
* - 只有规则的创建者可以删除
* - 删除操作不可恢复,请谨慎操作
*
* @param id 规则ID
*
* @return 成功时返回空 Unit失败时返回错误信息
*
* 权限要求:
* - 必须是规则的创建者creator_type 为 "user" 且 create_id 匹配)
*
* 示例:
* ```kotlin
* val response = api.deletePromptRule(456)
* ```
*/
@DELETE("outside/prompt/rule/{id}")
suspend fun deletePromptRule(
@Path("id") id: Int
): Response<Unit>
/**
* 查询智能体规则列表
*
* 功能说明:
* - 查询指定智能体下当前用户创建的规则列表
* - 支持分页和关键词模糊搜索
* - 只返回当前用户创建的规则,不会返回其他用户的规则
*
* @param promptId 智能体ID支持数字ID或UUID格式的 openId
* @param rule 规则内容关键词(模糊搜索),可选
* @param page 页码,默认 1
* @param pageSize 每页数量,默认 10
*
* @return 返回分页的规则列表,包含规则详情和关联的智能体信息
*
* 响应数据说明:
* - page: 当前页码
* - pageSize: 每页数量
* - total: 总记录数
* - list: 规则列表
* - id: 规则ID
* - rule: 规则内容
* - creator: 创建者名称
* - creator_type: 创建者类型
* - scope: 作用域
* - prompt: 关联的智能体信息id, title, avatar
* - created_at: 创建时间
* - updated_at: 更新时间
*
* 示例:
* ```kotlin
* // 使用数字ID查询
* val response1 = api.getPromptRuleList("123", page = 1, pageSize = 10)
*
* // 使用 OpenID 查询
* val response2 = api.getPromptRuleList(
* "550e8400-e29b-41d4-a716-446655440000",
* page = 1,
* pageSize = 10
* )
*
* // 带关键词搜索
* val response3 = api.getPromptRuleList(
* "123",
* rule = "政治",
* page = 1,
* pageSize = 10
* )
* ```
*/
@GET("outside/prompt/{promptId}/rule/list")
suspend fun getPromptRuleList(
@Path("promptId") promptId: String,
@Query("rule") rule: String? = null,
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 10
): Response<DataContainer<PromptRuleListResponse>>
/**
* 查询智能体规则配额信息
*
* 功能说明:
* - 查询指定智能体的规则条数使用情况
* - 包括基础配额、已购买配额、已使用数量等完整信息
* - 用于判断是否还能创建新规则
*
* @param promptId 智能体ID支持数字ID或UUID格式的 openId
*
* @return 返回配额详细信息
*
* 响应数据说明:
* - promptId: 智能体ID
* - promptTitle: 智能体标题
* - baseMaxCount: 基础条数限制(免费配额,由智能体等级决定)
* - purchasedCount: 用户购买的额外条数
* - totalMaxCount: 总可用条数baseMaxCount + purchasedCount
* - currentCount: 当前已创建的规则条数(只统计当前用户创建的)
* - remainingCount: 剩余可用条数totalMaxCount - currentCount
* - usagePercent: 使用百分比0-100currentCount / totalMaxCount * 100
*
* 使用场景:
* 1. 创建规则前检查是否有足够配额
* 2. 展示规则使用情况统计
* 3. 提示用户购买额外配额
*
* 示例:
* ```kotlin
* // 使用数字ID查询
* val response1 = api.getPromptRuleQuota("123")
*
* // 使用 OpenID 查询
* val response2 = api.getPromptRuleQuota("550e8400-e29b-41d4-a716-446655440000")
*
* // 处理响应
* response1.body()?.data?.let { quota ->
* if (quota.remainingCount > 0) {
* // 可以创建新规则
* println("还可以创建 ${quota.remainingCount} 条规则")
* } else {
* // 配额已用完
* println("规则配额已用完,已使用 ${quota.currentCount}/${quota.totalMaxCount}")
* }
* }
* ```
*/
@GET("outside/prompt/{promptId}/rule/count")
suspend fun getPromptRuleQuota(
@Path("promptId") promptId: String
): Response<DataContainer<PromptRuleQuota>>
@POST("outside/generate/agent-info")
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
}

View File

@@ -300,20 +300,34 @@ data class MomentEntity(
var relMoment: MomentEntity? = null,
// 是否收藏
var isFavorite: Boolean = false,
// 新闻相关字段
val isNews: Boolean = false,
val newsTitle: String = "",
val newsUrl: String = "",
val newsSource: String = "",
val newsCategory: String = "",
val newsLanguage: String = "",
val newsContent: String = ""
// 是否为新闻
val isNews: Boolean? = null,
// 新闻标题
val newsTitle: String? = null,
// 新闻链接
val newsUrl: String? = null,
// 新闻来源
val newsSource: String? = null,
// 新闻分类
val newsCategory: String? = null,
// 新闻语言
val newsLanguage: String? = null,
// 新闻内容
val newsContent: String? = null,
// 是否有完整文本
val hasFullText: Boolean? = null,
// 摘要
val summary: String? = null,
// 发布时间
val publishedAt: String? = null,
// 图片是否已缓存
val imageCached: Boolean? = null
)
class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null,
val newsOnly: Boolean? = null
val newsOnly: Boolean? = false
)
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData(
@@ -365,18 +379,6 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
onListChanged?.invoke(this.list)
}
fun updateCommentCount(id: Int, delta: Int) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
momentItem.copy(commentCount = newCount)
} else {
momentItem
}
}.toMutableList()
onListChanged?.invoke(this.list)
}
fun removeMoment(id: Int) {
this.list = this.list.filter { it.id != id }.toMutableList()
onListChanged?.invoke(this.list)

View File

@@ -30,12 +30,6 @@ object AppStore {
AppState.appTheme = DarkThemeColors()
}
// load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)
if (savedBgUrl != null) {
AppState.chatBackgroundUrl = savedBgUrl
}
}
suspend fun saveData() {
@@ -60,15 +54,5 @@ object AppStore {
}.apply()
}
fun saveChatBackgroundUrl(url: String?) {
sharedPreferences.edit().apply {
if (url != null) {
putString("chatBackgroundUrl", url)
} else {
remove("chatBackgroundUrl")
}
}.apply()
AppState.chatBackgroundUrl = url
}
}

View File

@@ -39,14 +39,13 @@ import com.aiosman.ravenow.ui.account.RemoveAccountScreen
import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
import com.aiosman.ravenow.ui.agent.CreateAgentV2Screen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.chat.GroupChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen
import com.aiosman.ravenow.ui.comment.notice.CommentNoticeScreen
import com.aiosman.ravenow.ui.composables.AgentCreatedSuccessIndicator
import com.aiosman.ravenow.ui.crop.ImageCropScreen
import com.aiosman.ravenow.ui.favourite.FavouriteListPage
import com.aiosman.ravenow.ui.favourite.FavouriteNoticeScreen
@@ -72,7 +71,6 @@ import com.aiosman.ravenow.ui.post.NewPostScreen
import com.aiosman.ravenow.ui.post.PostScreen
import com.aiosman.ravenow.ui.profile.AccountProfileV2
import com.aiosman.ravenow.ui.index.tabs.profile.vip.VipSelPage
import com.aiosman.ravenow.ui.notification.NotificationScreen
sealed class NavigationRoute(
val route: String,
@@ -107,7 +105,6 @@ sealed class NavigationRoute(
data object FavouriteList : NavigationRoute("FavouriteList")
data object Chat : NavigationRoute("Chat/{id}")
data object ChatAi : NavigationRoute("ChatAi/{id}")
data object ChatSetting : NavigationRoute("ChatSetting")
data object ChatGroup : NavigationRoute("ChatGroup/{id}/{name}/{avatar}")
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
data object ImageCrop : NavigationRoute("ImageCrop")
@@ -119,7 +116,6 @@ sealed class NavigationRoute(
data object GroupInfo : NavigationRoute("GroupInfo/{id}")
data object VipSelPage : NavigationRoute("VipSelPage")
data object RemoveAccountScreen: NavigationRoute("RemoveAccount")
data object NotificationScreen : NavigationRoute("NotificationScreen")
}
@@ -493,14 +489,6 @@ fun NavigationController(
}
}
composable(route = NavigationRoute.ChatSetting.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
ChatSettingScreen()
}
}
composable(
route = NavigationRoute.ChatGroup.route,
arguments = listOf(navArgument("id") { type = NavType.StringType },
@@ -557,7 +545,7 @@ fun NavigationController(
composable(
route = NavigationRoute.AddAgent.route,
) {
AddAgentScreen()
CreateAgentV2Screen()
}
composable(
@@ -603,13 +591,6 @@ fun NavigationController(
}
}
composable(route = NavigationRoute.NotificationScreen.route) {
CompositionLocalProvider(
LocalAnimatedContentScope provides this,
) {
NotificationScreen()
}
}
}
@@ -635,7 +616,6 @@ fun Navigation(
navController = navController,
startDestination = startDestination
)
AgentCreatedSuccessIndicator()
}
}
}

View File

@@ -1,8 +1,5 @@
package com.aiosman.ravenow.ui.account
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -50,24 +47,12 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import java.io.File
/**
* 编辑用户资料界面
*/
@Composable
fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) {
fun AccountEditScreen2() {
val model = AccountEditViewModel
val navController = LocalNavController.current
val context = LocalContext.current
@@ -76,19 +61,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 防抖导航器
val debouncedNavigation = rememberDebouncedNavigation()
// 添加图片选择启动器
val scope = rememberCoroutineScope()
val pickBannerImageLauncher = pickupAndCompressLauncher(
context,
scope,
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
quality = 100
) { uri, file ->
// 处理选中的图片
onUpdateBanner?.invoke(uri, file, context)
}
fun onNicknameChange(value: String) {
// 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
@@ -180,60 +152,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
)
}
}
// 添加横幅图片区域
val banner = model.profile?.banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.clip(RoundedCornerShape(12.dp))
) {
if (banner != null) {
CustomAsyncImage(
context = LocalContext.current,
imageUrl = banner,
modifier = Modifier.fillMaxSize(),
contentDescription = "Banner",
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.1f))
)
}
Box(
modifier = Modifier
.width(120.dp)
.height(42.dp)
.align(Alignment.BottomEnd)
.padding(end = 12.dp, bottom = 12.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(9.dp)
)
.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
){
Text(
text = "change",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(44.dp))
// 显示内容或加载状态
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
@@ -261,15 +180,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x997c68ef),
Color(0xFF7bd8f8)
)
),
)
.background(appColors.main)
.align(Alignment.BottomEnd)
.debouncedClickable(
debounceTime = 800L
@@ -287,7 +198,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
)
}
}
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(58.dp))
Column(
modifier = Modifier
.weight(1f)

View File

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

View File

@@ -24,12 +24,11 @@ object AddAgentViewModel : ViewModel() {
var croppedBitmap by mutableStateOf<Bitmap?>(null)
var isUpdating by mutableStateOf(false)
var isSelectingAvatar by mutableStateOf(false) // 标记是否正在选择头像
var showManualCreationForm by mutableStateOf(false)
var isCreatingAgent by mutableStateOf(false)
var showWaveAnimation by mutableStateOf(false)
var showManualCreation by mutableStateOf(false)
// 添加一个标志来区分两种手动表单状态
var isAutoModeManualForm by mutableStateOf(false)
var hasExitedPage by mutableStateOf(false) // 标记是否已经完全退出页面
// 保存AI生成的输入文本避免页面重建时丢失
var generateInputText by mutableStateOf("")
suspend fun updateAgentAvatar(context: Context) {
croppedBitmap?.let {
val file = File(context.cacheDir, "agent_avatar.jpg")
@@ -75,7 +74,7 @@ object AddAgentViewModel : ViewModel() {
name.length < 2 -> "智能体名称长度不能少于2个字符"
name.length > 20 -> "智能体名称长度不能超过20个字符"
desc.isEmpty() -> "智能体描述不能为空"
desc.length > 512 -> "智能体描述长度不能超过512个字符"
desc.length > 100 -> "智能体描述长度不能超过100个字符"
else -> null
}
}
@@ -89,10 +88,7 @@ object AddAgentViewModel : ViewModel() {
croppedBitmap = null
isUpdating = false
isSelectingAvatar = false
showManualCreationForm = false
isCreatingAgent = false
showWaveAnimation = false
showManualCreation = false
isAutoModeManualForm = false
hasExitedPage = false
generateInputText = ""
}
}

View File

@@ -0,0 +1,648 @@
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.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
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.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.animation.core.*
import androidx.compose.ui.geometry.Offset
import kotlin.math.cos
import kotlin.math.sin
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
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.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun LoadingDots(
modifier: Modifier = Modifier,
dotColor: Color = Color.Gray
) {
val infiniteTransition = rememberInfiniteTransition(label = "loading_dots")
val animationValues = (0..2).map { index ->
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 600,
easing = EaseInOut,
delayMillis = index * 200
),
repeatMode = RepeatMode.Reverse
),
label = "dot_$index"
)
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
animationValues.forEach { animValue ->
Box(
modifier = Modifier
.size(6.dp)
.offset(y = (-8 * animValue.value).dp)
.background(
color = dotColor.copy(alpha = 0.5f + 0.5f * animValue.value),
shape = CircleShape
)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateAgentV2Screen(
viewModel: CreateAgentV2ViewModel = remember { CreateAgentV2ViewModel() }
) {
// 页面进入时的状态管理
LaunchedEffect(Unit) {
// 总是先同步状态
viewModel.syncStateOnResume()
}
// 页面退出时的处理
DisposableEffect(Unit) {
onDispose {
// 页面退出时,标记为已退出(除非是在选择头像)
if (!viewModel.isSelectingAvatar) {
viewModel.markPageExited()
}
}
}
val appColors = LocalAppTheme.current
val navController = LocalNavController.current
val context = LocalContext.current
// 获取当前用户名,如果没有则使用默认值
val userName = AppState.profile?.nickName ?: "用户"
// 渐变边框旋转动画
val infiniteTransition = rememberInfiniteTransition(label = "gradient_rotation")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 16000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "rotation_angle"
)
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.background)
) {
// 状态栏占位
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 = "返回",
colorFilter = ColorFilter.tint(appColors.text),
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateUp()
}
)
Spacer(modifier = Modifier.width(12.dp))
// 标题 - 左对齐
Text(
text = "创建AI",
fontSize = 18.sp,
fontWeight = FontWeight.W700,
color = appColors.text
)
}
// 主要内容区域 - 可滚动
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.Start
) {
Spacer(modifier = Modifier.height(40.dp))
// AI头像图标
Box(
modifier = Modifier
.size(48.dp)
.background(
color = appColors.inputBackground,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_create_head_logo),
contentDescription = "AI头像",
modifier = Modifier.size(48.dp),
)
}
Spacer(modifier = Modifier.height(32.dp))
// 问候语
Text(
text = "$userName 你好呀!今天想创建什么?",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = appColors.text,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(16.dp))
// 描述性文字
Text(
text = "只需要一句话你的专属AI在这里诞生",
fontSize = 14.sp,
color = appColors.secondaryText,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(40.dp))
// 根据模式显示不同的UI
if (!viewModel.isManualMode) {
// AI生成模式 - 渐变边框输入框
Box(
modifier = Modifier
.fillMaxWidth()
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.4f),
spotColor = Color(0xFFd80264).copy(alpha = 0.4f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(1.5.dp) // 边框宽度
) {
// 内部输入框
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = appColors.background,
shape = RoundedCornerShape(15.dp)
)
.padding(8.dp)
) {
Column {
TextField(
value = viewModel.inputText,
onValueChange = {
if (!viewModel.isGenerating) {
viewModel.updateInputText(it)
}
},
placeholder = {
Text(
text = "一个会写诗的AI一个会懂你笑点的AI",
color = appColors.inputHint,
fontSize = 14.sp
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text,
disabledTextColor = appColors.inputHint,
cursorColor = if (viewModel.isGenerating) Color.Transparent else appColors.main,
focusedPlaceholderColor = appColors.inputHint,
unfocusedPlaceholderColor = appColors.inputHint,
disabledPlaceholderColor = appColors.inputHint.copy(alpha = 0.5f)
),
enabled = !viewModel.isGenerating,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 100.dp)
)
// AI美化按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.generateAgentInfo()
}
},
enabled = viewModel.canGenerate() && !viewModel.isGenerating,
colors = ButtonDefaults.textButtonColors(
contentColor = if (viewModel.isGenerating) appColors.inputHint else Color(0xFF7c46ed),
disabledContentColor = appColors.inputHint
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(R.drawable.ic_create_agent_generate),
contentDescription = "AI美化图标",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(Color(0xFF7c46ed))
)
Text(
text = if (viewModel.isGenerating) "生成中..." else "AI美化",
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// AI生成中的loading状态
if (viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
LoadingDots(
dotColor = appColors.main
)
Text(
text = "正在为你构思",
fontSize = 14.sp,
color = appColors.secondaryText,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// 手动创造AI按钮 - 只在非生成状态下显示
if (!viewModel.isGenerating) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
OutlinedButton(
onClick = {
if (!viewModel.isGenerating) {
viewModel.enableManualMode()
}
},
enabled = !viewModel.isGenerating,
modifier = Modifier.height(40.dp),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, appColors.inputHint.copy(alpha = 0.3f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = appColors.secondaryText,
disabledContentColor = appColors.inputHint
),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "手动创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
} else {
// 手动模式 - "一句话创造AI"按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
Box(
modifier = Modifier
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(12.dp),
ambientColor = Color(0xFF6246ff).copy(alpha = 0.3f),
spotColor = Color(0xFFd80264).copy(alpha = 0.3f)
)
) {
// 渐变边框
Box(
modifier = Modifier
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6246ff), // 紫色
Color(0xFFd80264), // 红色
Color(0xFF6246ff), // 紫色
Color(0xFFd80264) // 红色
),
start = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble())).toFloat() * 1000f
),
end = Offset(
x = cos(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f,
y = sin(Math.toRadians(rotationAngle.toDouble() + 180)).toFloat() * 1000f
)
),
shape = RoundedCornerShape(12.dp)
)
.padding(1.dp) // 边框宽度
) {
// 内部按钮
Box(
modifier = Modifier
.background(
color = appColors.background,
shape = RoundedCornerShape(11.dp)
)
.noRippleClickable {
viewModel.disableManualMode()
}
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = "编辑图标",
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
Text(
text = "一句话创造AI",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = appColors.secondaryText
)
}
}
}
}
}
}
// 生成结果显示区域
if (viewModel.hasGeneratedResult()) {
Spacer(modifier = Modifier.height(32.dp))
// 头像选择组件
Box(
modifier = Modifier
.size(72.dp)
.noRippleClickable {
viewModel.setSelectingAvatar(true)
navController.navigate(NavigationRoute.AgentImageCrop.route)
},
contentAlignment = Alignment.Center
) {
if (viewModel.croppedBitmap != null) {
// 有头像时显示头像
CustomAsyncImage(
context,
viewModel.croppedBitmap,
modifier = Modifier
.size(72.dp)
.clip(CircleShape),
contentDescription = "AI头像",
contentScale = ContentScale.Crop,
placeholderRes = R.mipmap.rider_pro_agent_avatar,
showShimmer = false
)
} else {
// 没有头像时显示渐变背景和编辑图标
Box(
modifier = Modifier
.size(72.dp)
.clip(CircleShape)
.background(
brush = Brush.verticalGradient(
0f to Color(0xFF7c45ed),
0.24f to Color(0xFF7c68ef),
1f to Color(0xFF7bd8f8)
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Edit,
contentDescription = "选择头像",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
// 标题输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "名称",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentTitle,
onValueChange = { viewModel.updateAgentTitle(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(16.dp))
// 描述输入框
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "描述",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = viewModel.agentDescription,
onValueChange = { viewModel.updateAgentDescription(it) },
colors = TextFieldDefaults.colors(
focusedContainerColor = appColors.inputBackground,
unfocusedContainerColor = appColors.inputBackground,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedTextColor = appColors.text,
unfocusedTextColor = appColors.text
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 120.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
// 错误信息显示
viewModel.errorMessage?.let { error ->
Text(
text = error,
color = Color.Red,
modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(16.dp))
}
// 创建AI按钮
ActionButton(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF7bd8f8),
Color(0xFF7c68ef),
Color(0xFF7c45ed)
)
),
shape = RoundedCornerShape(24.dp)
),
color = Color.White,
backgroundColor = Color.Transparent,
text = "好的,就它了",
isLoading = viewModel.isCreating,
loadingText = "创建中...",
enabled = viewModel.canCreate()
) {
viewModel.createAgent(context) {
navController.popBackStack()
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -0,0 +1,238 @@
package com.aiosman.ravenow.ui.agent
import android.content.Context
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.api.ApiClient
import com.aiosman.ravenow.data.api.GenerateAgentInfoRequestBody
import kotlinx.coroutines.launch
class CreateAgentV2ViewModel : ViewModel() {
// UI状态
var inputText by mutableStateOf("")
private set
var agentTitle by mutableStateOf("")
private set
var agentDescription by mutableStateOf("")
private set
var isGenerating by mutableStateOf(false)
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isCreating by mutableStateOf(false)
private set
var isManualMode by mutableStateOf(false)
private set
// 临时保存的生成结果,用于在生成过程中暂时隐藏当前结果
private var tempAgentTitle by mutableStateOf("")
private var tempAgentDescription by mutableStateOf("")
// AddAgentViewModel实例用于头像和创建逻辑
private val addAgentViewModel = AddAgentViewModel
// 获取头像相关状态
val croppedBitmap get() = addAgentViewModel.croppedBitmap
val isSelectingAvatar get() = addAgentViewModel.isSelectingAvatar
init {
// 初始化时检查是否需要恢复状态
if (addAgentViewModel.hasExitedPage) {
// 如果之前已经完全退出页面,清空所有数据
addAgentViewModel.clearData()
} else {
// 否则恢复已有状态(包括从头像选择回来的情况)
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
// 恢复输入文本
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun updateInputText(text: String) {
inputText = text
addAgentViewModel.generateInputText = text // 同时保存到AddAgentViewModel
clearError()
}
fun updateAgentTitle(title: String) {
agentTitle = title
syncToAddAgentViewModel()
clearError()
}
fun updateAgentDescription(description: String) {
agentDescription = description
syncToAddAgentViewModel()
clearError()
}
private fun clearError() {
errorMessage = null
}
private fun syncToAddAgentViewModel() {
addAgentViewModel.name = agentTitle
addAgentViewModel.desc = agentDescription
}
fun setSelectingAvatar(isSelecting: Boolean) {
addAgentViewModel.isSelectingAvatar = isSelecting
}
fun markPageExited() {
addAgentViewModel.hasExitedPage = true
}
fun syncStateOnResume() {
// 如果之前在选择头像,现在回来了,重置选择状态
if (addAgentViewModel.isSelectingAvatar) {
addAgentViewModel.isSelectingAvatar = false
// 从头像选择页面回来,恢复文本状态
if (addAgentViewModel.name.isNotEmpty()) {
agentTitle = addAgentViewModel.name
}
if (addAgentViewModel.desc.isNotEmpty()) {
agentDescription = addAgentViewModel.desc
}
if (addAgentViewModel.generateInputText.isNotEmpty()) {
inputText = addAgentViewModel.generateInputText
}
}
}
fun enableManualMode() {
isManualMode = true
// 手动模式下,如果没有现有内容,初始化为空
if (agentTitle.isEmpty() && agentDescription.isEmpty()) {
agentTitle = ""
agentDescription = ""
}
}
fun disableManualMode() {
isManualMode = false
}
fun generateAgentInfo() {
if (inputText.isBlank() || isGenerating) return
viewModelScope.launch {
try {
isGenerating = true
clearError()
// 开始生成时,暂存当前结果并清空显示
tempAgentTitle = agentTitle
tempAgentDescription = agentDescription
agentTitle = ""
agentDescription = ""
val response = ApiClient.longTimeoutApi.generateAgentInfo(
GenerateAgentInfoRequestBody(inputText)
)
if (response.isSuccessful) {
val data = response.body()?.data
data?.let {
// 成功时,使用新结果
agentTitle = it.title
agentDescription = it.description
syncToAddAgentViewModel()
// 清空临时保存
tempAgentTitle = ""
tempAgentDescription = ""
}
} else {
// 失败时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "生成失败,请重试"
}
} catch (e: Exception) {
// 异常时,恢复之前的结果
agentTitle = tempAgentTitle
agentDescription = tempAgentDescription
tempAgentTitle = ""
tempAgentDescription = ""
errorMessage = "网络错误: ${e.message}"
} finally {
isGenerating = false
}
}
}
fun createAgent(context: Context, onSuccess: () -> Unit) {
if (isCreating) return
viewModelScope.launch {
try {
isCreating = true
clearError()
// 验证输入
val validationError = addAgentViewModel.validate()
if (validationError != null) {
errorMessage = validationError
return@launch
}
// 调用创建智能体API
val result = addAgentViewModel.createAgent(context)
if (result != null) {
// 创建成功,清空数据
clearData()
onSuccess()
} else {
errorMessage = "创建失败,请重试"
}
} catch (e: Exception) {
errorMessage = "创建智能体失败: ${e.message}"
} finally {
isCreating = false
}
}
}
fun clearData() {
inputText = ""
agentTitle = ""
agentDescription = ""
errorMessage = null
isGenerating = false
isCreating = false
addAgentViewModel.clearData()
}
// 检查是否可以创建
fun canCreate(): Boolean {
return !isCreating && agentTitle.isNotBlank() && agentDescription.isNotBlank()
}
// 检查是否可以生成
fun canGenerate(): Boolean {
return !isGenerating && inputText.isNotBlank()
}
// 检查是否有生成结果或处于手动模式
fun hasGeneratedResult(): Boolean {
return agentTitle.isNotEmpty() || agentDescription.isNotEmpty() || isManualMode
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -20,7 +19,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@@ -39,8 +37,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -71,47 +67,7 @@ fun FollowerListScreen(userId: Int) {
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if (users.itemCount == 0) {
if (users.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -125,7 +81,7 @@ fun FollowerListScreen(userId: Int) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
else R.mipmap.invalid_name_8),
else R.mipmap.qst_fs_qs_img),
contentDescription = null,
modifier = Modifier.size(181.dp)
)

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -38,9 +37,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
/**
* 关注消息列表
@@ -57,51 +54,19 @@ fun FollowerNoticeScreen() {
val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow
var followers = dataFlow.collectAsLazyPagingItems()
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
.background(color = AppColors.background)
) {
NoticeScreenHeader(stringResource(R.string.followers_upper), moreIcon = false)
}
LaunchedEffect(Unit) {
model.reload()
model.updateNotice()
}
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.reload(force = true)
}
)
}
}
} else if (followers.itemCount == 0) {
if (followers.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -115,7 +80,7 @@ fun FollowerNoticeScreen() {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_fs_qs_as_img
else R.mipmap.invalid_name_8),
else R.mipmap.qst_fs_qs_img),
contentDescription = "No Followers",
modifier = Modifier.size(181.dp)
)

View File

@@ -20,7 +20,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@@ -38,9 +37,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -72,48 +69,7 @@ fun FollowingListScreen(userId: Int) {
NoticeScreenHeader(stringResource(R.string.following_upper), moreIcon = false)
}
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
}
}
} else if(users.itemCount == 0) {
if(users.itemCount == 0) {
Box(
modifier = Modifier
.fillMaxSize()
@@ -127,7 +83,7 @@ fun FollowingListScreen(userId: Int) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qst_gz_qs_as_img_my
else R.mipmap.invalid_name_9),
else R.mipmap.qst_gz_qs_img_my),
contentDescription = null,
modifier = Modifier.size(181.dp)
)

View File

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

View File

@@ -276,15 +276,12 @@ fun IndexScreen() {
bottomBar = {
NavigationBar(
modifier = Modifier.height(58.dp + navigationBarHeight),
containerColor = AppColors.tabUnselectedBackground
containerColor = AppColors.background
) {
item.forEachIndexed { idx, it ->
val isSelected = model.tabIndex == idx
// 定义新的选中颜色
val selectedColor = Color(0xFF7C45ED)
val iconTint by animateColorAsState(
targetValue = if (isSelected) selectedColor else AppColors.text,
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
animationSpec = tween(durationMillis = 250), label = ""
)
@@ -346,7 +343,7 @@ fun IndexScreen() {
.width(48.dp)
.height(32.dp)
.background(
color = if (isSelected) selectedColor.copy(alpha = 0.15f) else Color.Transparent,
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
@@ -365,7 +362,7 @@ fun IndexScreen() {
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) selectedColor else AppColors.text,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
)
}

View File

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

View File

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

View File

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

View File

@@ -91,14 +91,11 @@ fun AgentChatListScreen() {
horizontalAlignment = Alignment.CenterHorizontally,
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_znt_qs_as_img
else R.mipmap.invalid_name_5),
else R.mipmap.qs_znt_qs_img),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
@@ -117,35 +114,6 @@ fun AgentChatListScreen() {
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)
}
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()

View File

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

View File

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

View File

@@ -71,14 +71,11 @@ fun GroupChatListScreen() {
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_ql_qs_as_img
else R.mipmap.invalid_name_12),
else R.mipmap.qs_ql_qs_img),
contentDescription = "null data",
modifier = Modifier
.size(181.dp)
@@ -96,34 +93,6 @@ fun GroupChatListScreen() {
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 = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
}
} else {
LazyColumn(

View File

@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.index.tabs.message.tab
import android.content.Context
import android.icu.util.Calendar
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -168,12 +167,10 @@ object GroupChatListViewModel : ViewModel() {
}
fun createGroupChat(
trtcGroupId: String? = null,
roomId: Int? = null
trtcGroupId: String,
) {
viewModelScope.launch {
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId,roomId = roomId)
Log.d("debug",response.toString())
val response = ApiClient.api.createGroupChatAi(trtcGroupId = trtcGroupId)
}
}

View File

@@ -72,12 +72,9 @@ open class BaseMomentModel :ViewModel(){
}
fun onAddComment(id: Int) {
momentLoader.updateCommentCount(id, +1)
}
fun onDeleteComment(id: Int) {
momentLoader.updateCommentCount(id, -1)
suspend fun onAddComment(id: Int) {
// val currentPagingData = _momentsFlow.value
// updateCommentCount(id)
}
@@ -86,7 +83,6 @@ open class BaseMomentModel :ViewModel(){
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
}
suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
@@ -14,12 +13,11 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
@@ -41,7 +39,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.collectAsState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
@@ -107,8 +104,8 @@ fun DiscoverView() {
val isLoading by model.isLoading.collectAsState()
val context = LocalContext.current
val navController = LocalNavController.current
val gridState = rememberLazyStaggeredGridState()
val AppColors = LocalAppTheme.current
val gridState = rememberLazyGridState()
// 监听滚动到底部,自动加载更多
LaunchedEffect(gridState, moments.size) {
snapshotFlow {
@@ -127,40 +124,18 @@ fun DiscoverView() {
}
}
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState,
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp)
// contentPadding = PaddingValues(8.dp)
) {
items(moments) { momentItem ->
val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent
val textLines = if (textContent.isNotEmpty()) {
val estimatedCharsPerLine = 20
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行
} else {
0
}
val baseHeight = 200.dp
val singleLineTextHeight = 20.dp
val doubleLineTextHeight = 40.dp
val authorInfoHeight = 25.dp
val paddingHeight = 10.dp
val paddingHeight2 =3.dp
val totalHeight = baseHeight + when (textLines) {
0 -> authorInfoHeight + paddingHeight
1 -> singleLineTextHeight + authorInfoHeight + paddingHeight
else -> doubleLineTextHeight + authorInfoHeight +paddingHeight2
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight)
.aspectRatio(1f)
.padding(2.dp)
.noRippleClickable {
debouncer {
@@ -172,68 +147,14 @@ fun DiscoverView() {
}
}
) {
Column(
modifier = Modifier.fillMaxSize().background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
) {
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier
.fillMaxWidth()
.height(baseHeight)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
context = context,
showShimmer = true
)
Column(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight - baseHeight)
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
if (momentItem.momentTextContent.isNotEmpty()) {
androidx.compose.material3.Text(
text = momentItem.momentTextContent,
modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp,
color = AppColors.text,
maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
imageUrl = momentItem.avatar,
contentDescription = "",
modifier = Modifier
.size(16.dp)
.clip(RoundedCornerShape(8.dp)),
context = context,
showShimmer = true
)
androidx.compose.material3.Text(
text = momentItem.nickname,
modifier = Modifier.padding(start = 4.dp),
fontSize = 11.sp,
color = AppColors.text.copy(alpha = 0.6f),
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
}
}
CustomAsyncImage(
imageUrl = momentItem.images[0].thumbnail,
contentDescription = "",
modifier = Modifier
.fillMaxSize(),
context = context,
showShimmer = true
)
if (momentItem.images.size > 1) {
Box(
modifier = Modifier

View File

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

View File

@@ -0,0 +1,179 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.MomentCard
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import kotlinx.coroutines.launch
/**
* 新闻动态列表
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun News() {
val model = NewsViewModel
val moments = model.moments
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
model.refreshPager(
pullRefresh = true
)
})
val listState = rememberLazyListState()
// 用于跟踪是否已经触发过加载更多
var hasTriggeredLoadMore by remember { mutableStateOf(false) }
// observe list scrolling with simplified logic
val reachedBottom by remember {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
val totalItems = layoutInfo.totalItemsCount
if (lastVisibleItem == null || totalItems == 0) {
false
} else {
val isLastItemVisible = lastVisibleItem.index >= totalItems - 2
// 简化逻辑:只要滑动到底部且还没有触发过,就触发加载
isLastItemVisible && !hasTriggeredLoadMore
}
}
}
// load more if scrolled to bottom
LaunchedEffect(reachedBottom) {
if (reachedBottom) {
hasTriggeredLoadMore = true
model.loadMore()
}
}
LaunchedEffect(Unit) {
model.refreshPager()
}
// 监听数据变化,重置加载状态
LaunchedEffect(moments.size) {
if (moments.size > 0) {
// 延迟重置,确保数据已经稳定
kotlinx.coroutines.delay(500)
hasTriggeredLoadMore = false
}
}
Column(
modifier = Modifier
.fillMaxSize()
) {
Box(Modifier.pullRefresh(state)) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState
) {
items(
moments.size,
key = { idx -> idx }
) { idx ->
//处理下标越界
if (idx < 0 || idx >= moments.size) return@items
val momentItem = moments[idx] ?: return@items
val commentDebouncer = rememberDebouncer()
val likeDebouncer = rememberDebouncer()
val favoriteDebouncer = rememberDebouncer()
val followDebouncer = rememberDebouncer()
MomentCard(
momentEntity = momentItem,
onAddComment = {
commentDebouncer {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
model.onAddComment(momentItem.id)
}
}
}
},
onLikeClick = {
likeDebouncer {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
}
}
}
}
},
onFavoriteClick = {
favoriteDebouncer {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
}
}
},
onFollowClick = {
followDebouncer {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.FOLLOW_USER)) {
navController.navigate(NavigationRoute.Login.route)
} else {
model.followAction(momentItem)
}
}
},
showFollowButton = true
)
}
}
PullRefreshIndicator(
refreshing = model.refreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}

View File

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

View File

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

View File

@@ -2,17 +2,15 @@ package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
import org.greenrobot.eventbus.EventBus
object NewsViewModel : BaseMomentModel() {
init {
EventBus.getDefault().register(this)
}
override fun extraArgs(): MomentLoaderExtraArgs {
// 只拉取新闻
return MomentLoaderExtraArgs(
explore = false,
timelineId = null,
authorId = null,
newsOnly = true
)
return MomentLoaderExtraArgs(explore = true, newsOnly = true)
}
}

View File

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

View File

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

View File

@@ -50,12 +50,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
@@ -98,8 +96,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import androidx.compose.foundation.rememberScrollState
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -121,6 +117,7 @@ fun ProfileV3(
postCount: Int? = null, // 新增参数用于传递帖子总数
) {
val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { if (isAiAccount) 1 else 2 })
val enabled by remember { mutableStateOf(true) }
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
@@ -129,8 +126,6 @@ fun ProfileV3(
var showAgentMenu by remember { mutableStateOf(false) }
var contextAgent by remember { mutableStateOf<AgentEntity?>(null) }
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
var showOtherUserMenu by remember { mutableStateOf(false) }
var showBlockConfirmDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
@@ -154,19 +149,6 @@ fun ProfileV3(
val systemUiController = rememberSystemUiController()
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
val scrollState = rememberScrollState()
val toolbarAlpha by remember {
derivedStateOf {
if (!isSelf) {
1f
} else {
val maxScroll = 500f // 最大滚动距离,可调整
val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f)
progress
}
}
}
// observe list scrolling
val reachedListBottom by remember {
@@ -217,6 +199,8 @@ fun ProfileV3(
}
}
fun switchTheme() {
// delay
scope.launch {
@@ -295,151 +279,321 @@ fun ProfileV3(
Box(
modifier = Modifier.pullRefresh(refreshState)
) {
Column(
CollapsingToolbarScaffold(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.background(AppColors.profileBackground)
) {
// Banner
val banner = profile?.banner
if (banner != null) {
Box(
.background(AppColors.profileBackground),
state = state,
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
toolbarScrollable = true,
enabled = enabled,
toolbar = { toolbarScrollState ->
Column(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp)
.height(miniToolbarHeight.dp)
// 保持在最低高度和当前高度之间
.background(AppColors.profileBackground)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp - 24.dp)
.let {
if (isSelf && isMain) {
it.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(this)
}
}
} else {
it
}
}
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
),
)
) {
CustomAsyncImage(
LocalContext.current,
banner,
modifier = Modifier.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
} else {
Spacer(modifier = Modifier.height(100.dp))
}
// 用户信息
Box(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
profile?.let {
UserItem(
accountProfileEntity = it,
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// 操作按钮
profile?.let {
// header
Box(
modifier = Modifier
.parallax(0.5f)
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(if (isAiAccount) 600.dp else 700.dp)
.background(AppColors.profileBackground)
.verticalScroll(toolbarScrollState)
) {
if (isSelf) {
SelfProfileAction(
onEditProfile = {
navController.navigate(NavigationRoute.AccountEdit.route)
},
onPremiumClick = {
navController.navigate(NavigationRoute.VipSelPage.route)
}
)
} else {
if (it.id != AppState.UserId) {
OtherProfileAction(
it,
onFollow = {
onFollowClick()
},
onChat = {
onChatClick()
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = state.toolbarState.progress
}
)
) {
// banner
Box(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp)
.background(AppColors.profileBackground)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(bannerHeight.dp - 24.dp)
.let {
if (isSelf&&isMain) {
it.noRippleClickable {
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
pickBannerImageLauncher.launch(
this
)
}
}
} else {
it
}
}
.shadow(
elevation = 6.dp,
shape = RoundedCornerShape(
bottomStart = 32.dp,
bottomEnd = 32.dp
),
)
) {
val banner = profile?.banner
if (banner != null) {
CustomAsyncImage(
LocalContext.current,
banner,
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
} else {
Image(
painter = painterResource(id = R.drawable.rave_now_profile_backgrount_demo_1),
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop
)
}
}
if (isSelf&&isMain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 8.dp,
end = 8.dp
)
.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(
AppColors.background.copy(
alpha = 0.7f
)
)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = AppColors.text
)
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.profileBackground)
) {
// user info
Column(
modifier = Modifier
.fillMaxWidth()
) {
// Spacer(modifier = Modifier.height(16.dp))
// 个人信息
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
profile?.let {
UserItem(
accountProfileEntity = it,
postCount = postCount ?: if (isSelf) MyProfileViewModel.momentLoader.total else moments.size
)
}
}
Spacer(modifier = Modifier.height(20.dp))
profile?.let {
Box(
modifier = Modifier.padding(horizontal = 16.dp)
) {
if (isSelf) {
SelfProfileAction(
onEditProfile = {
navController.navigate(
NavigationRoute.AccountEdit.route
)
},
onPremiumClick = {
navController.navigate(NavigationRoute.VipSelPage.route)
}
)
} else {
if (it.id != AppState.UserId) {
OtherProfileAction(
it,
onFollow = {
onFollowClick()
},
onChat = {
onChatClick()
}
)
}
}
}
}
// 添加用户智能体行(智能体用户不显示)
if (!isAiAccount) {
UserAgentsRow(
userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp),
onMoreClick = {
// 导航到智能体列表页面
// TODO: 实现导航逻辑
},
onAgentClick = { agent ->
// 导航到智能体详情页面
// TODO: 实现导航逻辑
},
onAvatarClick = { agent ->
// 导航到智能体个人主页
scope.launch {
try {
val userService = com.aiosman.ravenow.data.UserServiceImpl()
val profile = userService.getUserProfileByOpenId(agent.openId)
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// 处理错误
}
}
},
onAgentLongClick = { agent ->
Log.d("ProfileV3", "onAgentLongClick called for agent: ${agent.title}, isSelf: $isSelf")
if (isSelf) { // 只有自己的智能体才能长按
Log.d("ProfileV3", "Setting contextAgent and showing menu")
contextAgent = agent
showAgentMenu = true
}
}
)
}
}
}
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = 1 - state.toolbarState.progress
}
.background(AppColors.profileBackground)
.onGloballyPositioned {
miniToolbarHeight = with(density) {
it.size.height.toDp().value.toInt()
}
}
) {
StatusBarSpacer()
Row(
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp,
).noRippleClickable {
},
verticalAlignment = Alignment.CenterVertically
) {
if (!isMain) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon), // Replace with your image resource
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSelf&&isMain) {
Box(
modifier = Modifier.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = AppColors.text
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
// 用户智能体行
if (!isAiAccount) {
UserAgentsRow(
userId = if (isSelf) null else profile?.id,
modifier = Modifier.padding(top = 16.dp),
onMoreClick = {
// 导航到智能体列表页面
},
onAgentClick = { agent ->
// 导航到智能体详情页面
},
onAvatarClick = { agent ->
// 导航到智能体个人主页
scope.launch {
try {
val userService = com.aiosman.ravenow.data.UserServiceImpl()
val profile = userService.getUserProfileByOpenId(agent.openId)
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// 处理错误
}
}
},
onAgentLongClick = { agent ->
Log.d("ProfileV3", "onAgentLongClick called for agent: ${agent.title}, isSelf: $isSelf")
if (isSelf) { // 只有自己的智能体才能长按
Log.d("ProfileV3", "Setting contextAgent and showing menu")
contextAgent = agent
showAgentMenu = true
}
}
)
}
// 内容
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxSize()
.background(AppColors.profileBackground)
.padding(top = 8.dp)
) {
UserContentPageIndicator(
pagerState = pagerState,
@@ -448,7 +602,6 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(8.dp))
HorizontalPager(
state = pagerState,
modifier = Modifier.height(500.dp) // 固定滚动高度
) { idx ->
when (idx) {
0 ->
@@ -478,196 +631,15 @@ fun ProfileV3(
}
}
}
}
// 顶部导航栏
TopNavigationBar(
isMain = isMain,
isSelf = isSelf,
profile = profile,
navController = navController,
alpha = toolbarAlpha,
onMenuClick = {
showOtherUserMenu = true
}
)
PullRefreshIndicator(
model.refreshing,
refreshState,
Modifier.align(Alignment.TopCenter)
)
// 其他用户菜单弹窗
if (showOtherUserMenu) {
ModalBottomSheet(
onDismissRequest = { showOtherUserMenu = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = Color.Transparent, // 设置容器背景透明
contentColor = Color.Transparent, // 设置内容背景透明
dragHandle = null, // 移除拖拽手柄
windowInsets = androidx.compose.foundation.layout.WindowInsets(0) // 移除窗口边距
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
) {
OtherUserMenuModal(
onBlockClick = {
showBlockConfirmDialog = true
},
onReportClick = {
// 实现举报逻辑
},
onCancelClick = {
showOtherUserMenu = false
},
onDismiss = { showOtherUserMenu = false }
)
}
}
// 拉黑确认弹窗
if (showBlockConfirmDialog) {
ModalBottomSheet(
onDismissRequest = {
showBlockConfirmDialog = false
},
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
) {
BlockConfirmDialog(
userProfile = profile,
onConfirmBlock = {
// 实现拉黑逻辑
},
onDismiss = {
showBlockConfirmDialog = false
showOtherUserMenu = false
}
)
}
}
}
}
}
}
//顶部导航栏组件
@Composable
fun TopNavigationBar(
isMain: Boolean,
isSelf: Boolean,
profile: AccountProfileEntity?,
navController: androidx.navigation.NavController,
alpha: Float,
onMenuClick: () -> Unit = {}
) {
val appColors = LocalAppTheme.current
Box(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer { this.alpha = alpha }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(appColors.profileBackground)
) {
StatusBarSpacer()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
},
verticalAlignment = Alignment.CenterVertically
) {
if (!isMain) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
navController.navigateUp()
}
.size(24.dp),
colorFilter = ColorFilter.tint(appColors.text)
)
Spacer(modifier = Modifier.width(8.dp))
CustomAsyncImage(
LocalContext.current,
profile?.avatar,
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = profile?.nickName ?: "",
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = appColors.text
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSelf && isMain) {
Box(
modifier = Modifier
.size(24.dp)
.padding(16.dp)
)
} else if (!isSelf) {
Box(
modifier = Modifier
.noRippleClickable {
onMenuClick()
}
.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "菜单",
tint = appColors.text,
modifier = Modifier.size(24.dp)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
if (isSelf && isMain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 32.dp, end = 16.dp)
.noRippleClickable {
IndexViewModel.openDrawer = true
}
) {
Box(
modifier = Modifier.padding(16.dp)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
tint = appColors.text
)
}
}
}
}
}
/**
@@ -749,7 +721,7 @@ fun AgentMenuModal(
if (isSelf) {
MenuActionItem(
icon = R.drawable.rider_pro_moment_delete,
text = stringResource(R.string.delete)
text = "删除"
) {
onDeleteClick()
}
@@ -825,96 +797,3 @@ fun DeleteConfirmDialog(
containerColor = AppColors.background
)
}
/**
* 其他用户主页菜单弹窗
*/
@Composable
fun OtherUserMenuModal(
onBlockClick: () -> Unit = {},
onReportClick: () -> Unit = {},
onCancelClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(bottom = 11.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(8.dp)
)
) {
// 拉黑选项
androidx.compose.material3.TextButton(
onClick = {
onBlockClick()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.block),
color = AppColors.error,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
// 分割线
androidx.compose.material3.HorizontalDivider(
color = AppColors.divider,
thickness = 0.5.dp
)
// 举报选项
androidx.compose.material3.TextButton(
onClick = {
onReportClick()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.report),
color = AppColors.error,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// 取消按钮
androidx.compose.material3.TextButton(
onClick = {
onCancelClick()
onDismiss()
},
modifier = Modifier
.fillMaxWidth()
.background(
color = AppColors.background,
shape = RoundedCornerShape(8.dp)
)
) {
Text(
stringResource(R.string.cancel),
color = AppColors.text,
fontSize = 16.sp
)
}
}
}
}

View File

@@ -37,16 +37,9 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils
@Composable
fun GalleryItem(
moment: MomentEntity,
@@ -136,50 +129,8 @@ fun GalleryGrid(
val AppColors = LocalAppTheme.current
val gridState = rememberLazyGridState()
val debouncer = rememberDebouncer()
var refreshKey by remember { mutableStateOf(0) }
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
refreshKey++
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
} else if (moments.isEmpty()) {
if (moments.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
@@ -190,7 +141,7 @@ fun GalleryGrid(
Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.qs_dt_qs_as_img
else R.mipmap.invalid_name_7),
else R.mipmap.qs_dt_qs_img),
contentDescription = "暂无图片",
modifier = Modifier.size(181.dp),
)

View File

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

View File

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

View File

@@ -19,7 +19,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -30,7 +29,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -46,10 +44,7 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils
@Composable
fun UserAgentsList(
@@ -201,7 +196,6 @@ fun UserAgentCard(
@Composable
fun EmptyAgentsView() {
val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
Column(
modifier = Modifier
@@ -209,65 +203,30 @@ fun EmptyAgentsView() {
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isNetworkAvailable) {
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.ai_dark
else R.mipmap.ai),
contentDescription = "暂无Agent",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
Image(
painter = painterResource(
id =if(AppState.darkMode) R.mipmap.qs_ai_qs_as_img
else R.mipmap.qs_ai_qs_img),
contentDescription = "暂无Agent",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "专属AI等你召唤",
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Text(
text = "专属AI等你召唤",
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "AI将成为你的伙伴而不是工具",
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
}
Text(
text = "AI将成为你的伙伴而不是工具",
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,8 +22,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -62,7 +60,7 @@ fun UserAuthScreen() {
val AppColors = LocalAppTheme.current
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var rememberMe by remember { mutableStateOf(true) }
var rememberMe by remember { mutableStateOf(false) }
val accountService: AccountService = AccountServiceImpl()
val captchaService: CaptchaService = CaptchaServiceImpl()
val scope = rememberCoroutineScope()
@@ -301,38 +299,32 @@ fun UserAuthScreen() {
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.lets_ride_upper),
backgroundBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0x777c68ef),
Color(0x777bd8f8)
)
),
backgroundColor = AppColors.main,
color = AppColors.mainText,
) {
onLogin()
}
// if (AppState.enableGoogleLogin) {
// Spacer(modifier = Modifier.height(16.dp))
// Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
// Spacer(modifier = Modifier.height(16.dp))
// ActionButton(
// modifier = Modifier.fillMaxWidth(),
// text = stringResource(R.string.sign_in_with_google),
// color = AppColors.text,
// leading = {
// Image(
// painter = painterResource(id = R.drawable.rider_pro_google),
// contentDescription = "Google",
// modifier = Modifier.size(36.dp)
// )
// },
// expandText = true,
// contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
// ) {
// googleLogin()
// }
// }
if (AppState.enableGoogleLogin) {
Spacer(modifier = Modifier.height(16.dp))
Text(stringResource(R.string.or_login_with), color = AppColors.secondaryText)
Spacer(modifier = Modifier.height(16.dp))
ActionButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.sign_in_with_google),
color = AppColors.text,
leading = {
Image(
painter = painterResource(id = R.drawable.rider_pro_google),
contentDescription = "Google",
modifier = Modifier.size(36.dp)
)
},
expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
) {
googleLogin()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.098,4.33 l1.475,-1.475a0.643,0.643 0,0 1,0.909 0l1.663,1.663a0.643,0.643 0,0 1,0 0.91L11.67,6.901M9.098,4.33l-6.243,6.242a0.643,0.643 0,0 0,-0.188 0.455v1.663c0,0.355 0.288,0.643 0.643,0.643h1.663c0.17,0 0.334,-0.067 0.455,-0.188l6.242,-6.243M9.098,4.33l2.572,2.572"
android:strokeLineJoin="round"
android:strokeWidth="1.333"
android:fillColor="#00000000"
android:strokeColor="#7C45ED"
android:fillType="evenOdd"
android:strokeLineCap="round"/>
<path
android:pathData="M3.608,6.333c0.13,0 0.202,-0.07 0.215,-0.208 0.056,-0.434 0.114,-0.775 0.172,-1.022 0.06,-0.247 0.152,-0.434 0.28,-0.56 0.128,-0.126 0.318,-0.221 0.57,-0.286 0.252,-0.066 0.599,-0.133 1.042,-0.202 0.152,-0.026 0.228,-0.1 0.228,-0.222 0,-0.06 -0.02,-0.11 -0.059,-0.146a0.304,0.304 0,0 0,-0.143 -0.075,15.533 15.533,0 0,1 -1.048,-0.225c-0.256,-0.067 -0.45,-0.161 -0.583,-0.283 -0.132,-0.121 -0.228,-0.304 -0.287,-0.547a10.18,10.18 0,0 1,-0.172 -1.015c-0.013,-0.14 -0.085,-0.209 -0.215,-0.209a0.221,0.221 0,0 0,-0.153 0.056,0.208 0.208,0 0,0 -0.068,0.146 9.948,9.948 0,0 1,-0.176 1.039c-0.06,0.25 -0.155,0.438 -0.283,0.566 -0.128,0.128 -0.32,0.224 -0.576,0.286 -0.256,0.063 -0.608,0.125 -1.055,0.186a0.251,0.251 0,0 0,-0.143 0.072,0.203 0.203,0 0,0 -0.059,0.15c0,0.06 0.02,0.109 0.059,0.146 0.039,0.037 0.086,0.062 0.143,0.075 0.447,0.082 0.799,0.158 1.055,0.228 0.256,0.069 0.448,0.166 0.576,0.29 0.128,0.123 0.221,0.306 0.28,0.55 0.058,0.242 0.118,0.581 0.179,1.015a0.202,0.202 0,0 0,0.068 0.14,0.221 0.221,0 0,0 0.153,0.055zM12.516,14.702c0.086,0 0.139,-0.05 0.156,-0.15 0.056,-0.308 0.108,-0.552 0.156,-0.732a0.963,0.963 0,0 1,0.208 -0.417,0.879 0.879,0 0,1 0.41,-0.225c0.183,-0.052 0.439,-0.106 0.769,-0.162 0.1,-0.018 0.15,-0.072 0.15,-0.163 0,-0.091 -0.05,-0.146 -0.15,-0.163a6.959,6.959 0,0 1,-0.768 -0.166,0.919 0.919,0 0,1 -0.41,-0.224 0.937,0.937 0,0 1,-0.209 -0.414c-0.048,-0.18 -0.1,-0.426 -0.156,-0.739 -0.017,-0.095 -0.07,-0.143 -0.156,-0.143 -0.091,0 -0.146,0.048 -0.163,0.143a9.897,9.897 0,0 1,-0.156 0.74,0.937 0.937,0 0,1 -0.209,0.413 0.919,0.919 0,0 1,-0.41 0.224c-0.182,0.054 -0.436,0.11 -0.762,0.166 -0.1,0.017 -0.15,0.072 -0.15,0.163 0,0.091 0.05,0.145 0.15,0.163 0.326,0.056 0.58,0.11 0.762,0.162a0.879,0.879 0,0 1,0.41 0.225c0.091,0.098 0.16,0.237 0.209,0.417 0.047,0.18 0.1,0.424 0.156,0.732 0.009,0.044 0.026,0.08 0.052,0.108a0.143,0.143 0,0 0,0.11 0.042z"
android:fillColor="#7C45ED"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M24,24m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
android:fillColor="#5E5CE6"
android:fillAlpha="0.1"
android:fillType="evenOdd"/>
<path
android:pathData="M29.132,11.3c0.861,0.075 1.609,0.484 2.124,1.082a3.019,3.019 50,0 1,-0.378 4.325c-0.448,0.365 -0.987,0.6 -1.56,0.68l-0.02,0.002 0.057,0.035c1.649,0.997 2.914,2.401 3.789,4.009l0.041,0.076c1.115,2.087 1.576,4.508 1.374,6.807 -0.237,2.703 -1.392,4.636 -3.188,5.873a3.07,3.07 50,0 1,0.025 1.749c-0.168,0.58 -0.476,0.756 -0.578,0.791 -0.176,0.059 -1.855,-0.393 -2.957,-1.133 -1.289,0.249 -2.726,0.304 -4.271,0.168 -3.173,-0.276 -6.14,-1.296 -8.154,-3.002 -1.84,-1.559 -2.906,-3.682 -2.672,-6.345 0.261,-2.968 1.606,-5.945 3.885,-7.972 1.978,-1.761 4.657,-2.815 7.951,-2.529 0.608,0.053 1.187,0.148 1.738,0.28l0.039,0.009 -0.01,-0.013a3.013,3.013 50,0 1,-0.627 -2.061l0.004,-0.051c0.074,-0.845 0.492,-1.579 1.109,-2.083a3.163,3.163 50,0 1,2.281 -0.698z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startX="23.669"
android:startY="11.286"
android:endX="23.669"
android:endY="36.734"
android:type="linear">
<item android:offset="0" android:color="#FF7C45ED"/>
<item android:offset="0.236" android:color="#FF7C68EF"/>
<item android:offset="1" android:color="#FF7BD8F8"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M17.51,22.904L17.736,22.924A0.912,0.912 50,0 1,18.565 23.912L18.419,25.584A0.912,0.912 50,0 1,17.431 26.413L17.205,26.393A0.912,0.912 50,0 1,16.376 25.405L16.522,23.733A0.912,0.912 50,0 1,17.51 22.904z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
<path
android:pathData="M22.163,23.341L22.389,23.361A0.912,0.912 50,0 1,23.218 24.349L23.072,26.021A0.912,0.912 50,0 1,22.083 26.85L21.857,26.83A0.912,0.912 50,0 1,21.028 25.842L21.175,24.17A0.912,0.912 50,0 1,22.163 23.341z"
android:fillColor="#FFF"
android:fillType="nonZero"/>
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="m9.185,2 l2.37,2.341 -6.518,6.44h-2.37V8.438zM2.667,13.707h10.667"
android:strokeLineJoin="round"
android:strokeWidth="1.185"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

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