添加新的表单文本输入组件 FormTextInput2,包含错误提示和动态显示功能;新增图标和图片资源。

This commit is contained in:
weber
2025-08-05 13:56:03 +08:00
parent 29d2bb753f
commit 3e544844bb
22 changed files with 712 additions and 116 deletions

View File

@@ -20,6 +20,7 @@ open class AppThemeData(
var decentBackground: Color,
var divider: Color,
var inputBackground: Color,
var inputBackground2: Color,
var inputHint: Color,
var error: Color,
var checkedBackground: Color,
@@ -30,7 +31,7 @@ open class AppThemeData(
)
class LightThemeColors : AppThemeData(
main = Color(0xffda3832),
main = Color(0xffD80264),
mainText = Color(0xffffffff),
basicMain = Color(0xfff0f0f0),
nonActive = Color(0xfff5f5f5),
@@ -43,6 +44,7 @@ class LightThemeColors : AppThemeData(
background = Color(0xFFFFFFFF),
divider = Color(0xFFEbEbEb),
inputBackground = Color(0xFFF7f7f7),
inputBackground2 = Color(0xFFFFFFFF),
inputHint = Color(0xffdadada),
error = Color(0xffFF0000),
checkedBackground = Color(0xff000000),
@@ -68,6 +70,7 @@ class DarkThemeColors : AppThemeData(
background = Color(0xFF121212),
divider = Color(0xFF282828),
inputBackground = Color(0xFF1C1C1C),
inputBackground2 = Color(0xFF1C1C1C),
inputHint = Color(0xff888888),
error = Color(0xffFF0000),
checkedBackground = Color(0xffffffff),

View File

@@ -148,6 +148,10 @@ interface MomentService {
relPostId: Int? = null
): MomentEntity
suspend fun agentMoment(
content: String,
): String
/**
* 收藏动态
* @param id 动态ID

View File

@@ -31,6 +31,12 @@ data class RegisterRequestBody(
@SerializedName("password")
val password: String
)
data class AgentMomentRequestBody(
@SerializedName("generateText")
val generateText: String,
@SerializedName("sessionId")
val sessionId: String
)
data class LoginUserRequestBody(
@SerializedName("username")
@@ -472,6 +478,17 @@ interface RaveNowAPI {
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<Agent>>
@Multipart
@POST("outside/prompts")
suspend fun createAgent(
@Part avatar: MultipartBody.Part?,
@Part("title") title: RequestBody?,
@Part("desc") desc: RequestBody?,
): Response<DataContainer<Agent>>
@POST("generate/postText")
suspend fun agentMoment(@Body body: AgentMomentRequestBody): Response<DataContainer<String>>
}

View File

@@ -5,14 +5,35 @@ import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.IOException
/**
* 智能体
*/
suspend fun createAgent(
title: String,
desc: String,
avatar: UploadImage,
): AgentEntity {
val textTitle = title.toRequestBody("text/plain".toMediaTypeOrNull())
val textDesc = desc.toRequestBody("text/plain".toMediaTypeOrNull())
val avatarField: MultipartBody.Part? = avatar?.let {
createMultipartBody(it.file, it.filename, "avatar")
}
val response = ApiClient.api.createAgent(avatarField, textTitle ,textDesc)
val body = response.body()?.data ?: throw ServiceException("Failed to create agent")
return body.toAgentEntity()
}
data class AgentEntity(
val author: String,
val avatar: String,
@@ -39,7 +60,10 @@ data class ProfileEntity(
val trtcUserId: String,
val username: String
)
fun createMultipartBody(file: File, filename: String, name: String): MultipartBody.Part {
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
return MultipartBody.Part.createFormData(name, filename, requestFile)
}
class AgentLoaderExtraArgs(
)

View File

@@ -7,6 +7,7 @@ import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.api.AgentMomentRequestBody
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.parseErrorResponse
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -127,6 +128,10 @@ class MomentServiceImpl() : MomentService {
return momentBackend.createMoment(content, authorId, images, relPostId)
}
override suspend fun agentMoment(content: String): String {
return momentBackend.agentMoment(content)
}
override suspend fun favoriteMoment(id: Int) {
momentBackend.favoriteMoment(id)
}
@@ -212,6 +217,17 @@ class MomentBackend {
}
suspend fun agentMoment(
content: String,
): String {
val textContent = content.toRequestBody("text/plain".toMediaTypeOrNull())
val sessionId = ""
val response = ApiClient.api.agentMoment(AgentMomentRequestBody(generateText = content, sessionId =sessionId ))
val body = response.body()?.data ?: throw ServiceException("Failed to agent moment")
return body.toString()
}
suspend fun favoriteMoment(id: Int) {
ApiClient.api.favoritePost(id)
}

View File

@@ -25,6 +25,7 @@ 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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -42,6 +43,7 @@ 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.composables.form.FormTextInput
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -84,12 +86,13 @@ fun AddAgentScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = appColors.background),
.background(color = appColors.decentBackground),
horizontalAlignment = Alignment.CenterHorizontally
) {
StatusBarSpacer()
Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
.background(color = appColors.decentBackground)
) {
ScreenHeader (
title = stringResource(R.string.agent_add),
@@ -150,15 +153,17 @@ fun AddAgentScreen() {
value = model.name,
label = stringResource(R.string.agent_name),
hint = stringResource(R.string.agent_name_hint),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onNicknameChange(value)
}
// Spacer(modifier = Modifier.height(16.dp))
FormTextInput(
FormTextInput2(
value = model.desc,
label = stringResource(R.string.agent_desc),
hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2,
modifier = Modifier.fillMaxWidth(),
) { value ->
onDescChange(value)
@@ -167,7 +172,20 @@ fun AddAgentScreen() {
Spacer(modifier = Modifier.height(58.dp))
ActionButton(
modifier = Modifier
.width(345.dp),
.width(345.dp)
.padding(horizontal = 16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFEE2A33),
Color(0xFFD80264),
Color(0xFF8468BC)
)
),
shape = RoundedCornerShape(24.dp)
),
color = Color.White,
backgroundColor = Color.Transparent,
text = stringResource(R.string.agent_create),
) {

View File

@@ -129,7 +129,7 @@ fun ScreenHeader(
)
Spacer(modifier = Modifier.size(12.dp))
Text(title,
fontWeight = FontWeight.W800,
fontWeight = FontWeight.W600,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
fontSize = 17.sp,

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
@@ -18,6 +19,7 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -26,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
@@ -58,12 +61,12 @@ fun <T : Any> DraggableGrid(
val dragDropState =
rememberGridDragDropState(gridState, onMove, onDragModeStart, onDragModeEnd, lockedIndices)
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.dragContainer(dragDropState),
columns = GridCells.Fixed(5),
modifier = Modifier.dragContainer(dragDropState).padding(horizontal = 8.dp),
state = gridState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(items, key = { _, item ->
@@ -122,7 +125,7 @@ fun LazyGridItemScope.DraggableItem(
} else {
Modifier.animateItemPlacement()
}
Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
Box(modifier = modifier.then(draggingModifier).clip(RoundedCornerShape(8.dp)), propagateMinConstraints = true) {
content(dragging)
}
}

View File

@@ -57,10 +57,29 @@ fun CustomAsyncImage(
val imageLoader = getImageLoader(context ?: localContext)
// 处理 imageUrl 为 null 的情况
if (imageUrl == null|| imageUrl == "") {
// 如果 imageUrl 为 null 且有占位符,则直接显示占位符
if (placeholderRes != null) {
androidx.compose.foundation.Image(
painter = androidx.compose.ui.res.painterResource(placeholderRes),
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale
)
return
}
}
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)
.data(imageUrl)
.crossfade(200)
.apply {
// 设置占位符图片
if (placeholderRes != null) {
placeholder(placeholderRes)
}
}
.build(),
contentDescription = contentDescription,
modifier = modifier,

View File

@@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
@@ -32,6 +33,9 @@ import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
/**
* 水平布局的输入框
*/
@Composable
fun FormTextInput(
modifier: Modifier = Modifier,
@@ -39,6 +43,7 @@ fun FormTextInput(
label: String? = null,
error: String? = null,
hint: String? = null,
background: Color? = null,
onValueChange: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
@@ -48,7 +53,7 @@ fun FormTextInput(
Row(
modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(AppColors.inputBackground)
.background(background ?: AppColors.inputBackground)
.let {
if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
@@ -66,7 +71,7 @@ fun FormTextInput(
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.W600,
color = AppColors.text
)
)

View File

@@ -0,0 +1,142 @@
package com.aiosman.ravenow.ui.composables.form
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
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.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
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
/**
* 垂直布局的输入框
*/
@Composable
fun FormTextInput2(
modifier: Modifier = Modifier,
value: String,
label: String? = null,
error: String? = null,
hint: String? = null,
background: Color? = null,
onValueChange: (String) -> Unit
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier.height(150.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(background ?: AppColors.inputBackground)
.let {
if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
} else {
it
}
}
.padding(17.dp),
) {
label?.let {
Text(
text = it,
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
)
}
Box(
modifier = Modifier
.weight(1f)
.padding(top = 8.dp)
) {
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),
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = error != null,
enter = fadeIn(),
exit = fadeOut()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.mipmap.rider_pro_input_error),
contentDescription = "Error",
modifier = Modifier.size(8.dp)
)
Spacer(modifier = Modifier.size(4.dp))
AnimatedContent(targetState = error) { targetError ->
Text(targetError ?: "", color = AppColors.error, fontSize = 12.sp)
}
}
}
}
}
}

View File

@@ -62,16 +62,16 @@ fun Agent() {
) {
Row(
modifier = Modifier
.height(36.dp) // 设置高度为36dp
.fillMaxWidth(), // 占据整行宽度
.height(36.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
// 搜索框 - 占据剩余空间
// 搜索框
Row(
modifier = Modifier
.height(36.dp)
.weight(1f) // 权重为1占据剩余空间
.weight(1f)
.clip(shape = RoundedCornerShape(18.dp))
.background(AppColors.inputBackground)
.padding(horizontal = 8.dp, vertical = 0.dp)
@@ -94,16 +94,13 @@ fun Agent() {
)
}
}
// 间隔
Spacer(modifier = Modifier.width(16.dp))
// 新增
Icon(
modifier = Modifier
.size(36.dp)
.noRippleClickable {
// 图标点击事件
//
navController.navigate(
NavigationRoute.AddAgent.route
)

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.message
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -14,7 +15,10 @@ 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.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@@ -28,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -60,13 +65,15 @@ import kotlinx.coroutines.launch
/**
* 消息列表界面
*/
@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable
fun NotificationsScreen() {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
var pagerState = rememberPagerState (pageCount = { 4 })
var scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch {
MessageListViewModel.initData(context, force = true, loadChat = AppState.enableChat)
@@ -96,10 +103,9 @@ fun NotificationsScreen() {
.padding(horizontal = 15.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧占位元素
Box(modifier = Modifier.size(24.dp))
// 左侧 Columnlabel 居中显示
Column(
modifier = Modifier
.weight(1f)
@@ -113,7 +119,7 @@ fun NotificationsScreen() {
color = AppColors.text
)
}
// 右侧图标
Image(
painter = painterResource(id = R.drawable.rider_pro_group),
contentDescription = "add",
@@ -169,8 +175,111 @@ fun NotificationsScreen() {
navController.navigate(NavigationRoute.CommentNoticeScreen.route)
}
}
HorizontalDivider(color = AppColors.divider, modifier = Modifier.padding(16.dp))
Box(
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(0)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.chat_ai),
fontSize = 14.sp,
color = if (pagerState.currentPage == 0) AppColors.mainText else AppColors.checkedBackground,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(if (pagerState.currentPage == 0) AppColors.checkedBackground else AppColors.unCheckedBackground)
.padding(horizontal = 11.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(1)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material.Text(
text = stringResource(R.string.chat_group),
fontSize = 14.sp,
color = if (pagerState.currentPage == 1) AppColors.mainText else AppColors.checkedBackground,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(if (pagerState.currentPage == 1) AppColors.checkedBackground else AppColors.unCheckedBackground)
.padding(horizontal = 11.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier
.noRippleClickable {
scope.launch {
pagerState.animateScrollToPage(2)
}
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material.Text(
text = stringResource(R.string.chat_friend),
fontSize = 14.sp,
color = if (pagerState.currentPage == 2) AppColors.mainText else AppColors.checkedBackground,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(if (pagerState.currentPage == 2) AppColors.checkedBackground else AppColors.unCheckedBackground)
.padding(horizontal = 11.dp, vertical = 4.dp)
)
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
when (it) {
0 -> {
}
1 -> {
}
2 -> {
}
}
}
/*Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
@@ -194,7 +303,7 @@ fun NotificationsScreen() {
)
}
}
}*/
}
PullRefreshIndicator(
MessageListViewModel.isLoading,
@@ -226,28 +335,14 @@ fun NotificationIndicator(
onClick()
}
) {
if (notificationCount > 0) {
Box(
modifier = Modifier
.background(AppColors.main, RoundedCornerShape(16.dp))
.padding(4.dp)
.align(Alignment.TopEnd)
) {
Text(
text = notificationCount.toString(),
color = AppColors.mainText,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Center)
)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.size(64.dp)
.size(69.dp)
.padding(5.dp)
.background(color = backgroundColor,
shape = RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
@@ -265,6 +360,22 @@ fun NotificationIndicator(
}
}
if (notificationCount > 0) {
Box(
modifier = Modifier
.background(AppColors.main, RoundedCornerShape(16.dp))
.padding(horizontal = 8.dp, vertical = 4.dp)
.align(Alignment.TopEnd)
) {
Text(
text = if (notificationCount > 99) "99+" else notificationCount.toString(),
color = AppColors.mainText,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}

View File

@@ -4,6 +4,11 @@ import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -31,6 +36,8 @@ import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -42,6 +49,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
@@ -49,20 +58,25 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.createAgent
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DraggableGrid
@@ -78,6 +92,10 @@ import java.io.File
@Composable
fun NewPostScreen() {
val AppColors = LocalAppTheme.current
var isAiEnabled by remember { mutableStateOf(false) }
var isRotating by remember { mutableStateOf(false) }
var isRequesting by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current // 添加这行
val model = NewPostViewModel
val systemUiController = rememberSystemUiController()
@@ -102,9 +120,6 @@ fun NewPostScreen() {
) {
NewPostTopBar {
}
NewPostTextField("Share your adventure…", NewPostViewModel.textContent) {
NewPostViewModel.textContent = it
}
Column(
modifier = Modifier
.fillMaxWidth()
@@ -128,7 +143,168 @@ fun NewPostScreen() {
}
AddImageGrid()
// AdditionalPostItem()
NewPostTextField(stringResource(R.string.moment_content_hint), NewPostViewModel.textContent) {
NewPostViewModel.textContent = it
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(AppColors.divider)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.mipmap.rider_pro_moment_ai),
contentDescription = null,
modifier = Modifier
.size(24.dp)
)
Text(
text = stringResource(R.string.moment_ai_co),
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
color = AppColors.text,
)
Switch(
checked = isAiEnabled,
onCheckedChange = {
isChecked ->
isAiEnabled = isChecked
if (isChecked) {
// 收起键盘
keyboardController?.hide()
isRequesting = true
isRotating = true
model.viewModelScope.launch {
try {
model.agentMoment(model.textContent)
} catch (e: Exception) {
e.printStackTrace()
}finally {
isRequesting = false
isRotating = false
isAiEnabled = false
}
}
} else {
}
},
enabled = !isRequesting && model.textContent.isNotEmpty(),
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = AppColors.brandColorsColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = Color(0xFFE9E9EA),
uncheckedBorderColor = Color.White,
disabledCheckedTrackColor = AppColors.brandColorsColor.copy(alpha = 0.8f),
disabledCheckedThumbColor= Color.White,
disabledUncheckedTrackColor = Color(0xFFE9E9EA),
disabledUncheckedThumbColor= Color.White
),
modifier = Modifier.scale(0.8f)
)
}
Column(
modifier = Modifier.fillMaxWidth()
) {
BasicTextField(
value = model.aiTextContent,
onValueChange = { newValue ->
model.aiTextContent = newValue
},
modifier = Modifier
.height(160.dp)
.heightIn(160.dp)
.padding(horizontal = 16.dp, vertical = 10.dp)
.fillMaxWidth(),
cursorBrush = SolidColor(AppColors.text),
textStyle = TextStyle(
lineHeight = 24.sp,
color = AppColors.text,
),
readOnly = true
)
if (model.aiTextContent.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.End // 靠右对齐
) {
// 删除按钮
Row(
modifier = Modifier
.noRippleClickable {
model.aiTextContent = ""
}
.background(
color = AppColors.basicMain,
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_moment_delete),
contentDescription = "delete",
modifier = Modifier.size(16.dp),
tint = AppColors.text
)
Text(
text = stringResource(R.string.moment_ai_delete),
fontSize = 12.sp,
color = AppColors.text,
modifier = Modifier.padding(start = 4.dp)
)
}
Spacer(modifier = Modifier.width(14.dp))
//应用生成文案
Row(
modifier = Modifier
.noRippleClickable {
if (model.aiTextContent.isNotEmpty()) {
model.textContent = model.aiTextContent
}
}
.background(
color = AppColors.basicMain,
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_moment_apply),
contentDescription = "apply",
modifier = Modifier.size(16.dp),
tint = AppColors.text
)
Text(
text = stringResource(R.string.moment_ai_apply),
fontSize = 12.sp,
color = AppColors.text,
modifier = Modifier.padding(start = 4.dp)
)
}
}
}
}
}
}
}
@@ -172,7 +348,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
modifier = Modifier.align(Alignment.CenterStart),
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
painter = painterResource(id = R.drawable.rider_pro_close),
contentDescription = "Back",
modifier = Modifier
.size(24.dp)
@@ -182,12 +358,11 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
colorFilter = ColorFilter.tint(AppColors.text)
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.rider_pro_video_share),
tint = AppColors.text,
Image(
painter = painterResource(id = R.mipmap.rider_pro_moment_post),
contentDescription = "Send",
modifier = Modifier
.size(32.dp)
.size(24.dp)
.noRippleClickable {
// 检查输入
val errorMessage = model.validateMoment()
@@ -208,7 +383,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
uploading = false
}
// 上传完成后隐藏进度条
}
}
@@ -227,13 +402,15 @@ fun NewPostTextField(hint: String, value: String, onValueChange: (String) -> Uni
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.heightIn(200.dp)
.padding(horizontal = 18.dp, vertical = 10.dp),
.height(160.dp)
.heightIn(160.dp)
.padding(horizontal = 16.dp, vertical = 10.dp),
cursorBrush = SolidColor(AppColors.text),
textStyle = TextStyle(
lineHeight = 24.sp,
color = AppColors.text,
)
),
)
if (value.isEmpty()) {
@@ -326,11 +503,11 @@ fun AddImageGrid() {
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(16.dp),
columns = GridCells.Fixed(5),
contentPadding = PaddingValues(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -339,18 +516,8 @@ fun AddImageGrid() {
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind {
val strokeWidth = 1.dp.toPx()
val dashLength = 10f
val dashGap = 10f
val pathEffect =
PathEffect.dashPathEffect(floatArrayOf(dashLength, dashGap))
drawRoundRect(
color = Color(0xFFD6D6D6),
style = Stroke(strokeWidth, pathEffect = pathEffect),
cornerRadius = CornerRadius(8.dp.toPx())
)
}
.clip(RoundedCornerShape(16.dp)) // 设置圆角
.background(Color(0xFFFAF9FB)) // 设置背景色
.noRippleClickable {
pickImagesLauncher.launch("image/*")
},
@@ -359,7 +526,7 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = "Add Image",
modifier = Modifier
.size(48.dp)
.size(24.dp)
.align(Alignment.Center),
tint = Color(0xFFD6D6D6)
@@ -371,18 +538,8 @@ fun AddImageGrid() {
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.drawBehind {
val strokeWidth = 1.dp.toPx()
val dashLength = 10f
val dashGap = 10f
val pathEffect =
PathEffect.dashPathEffect(floatArrayOf(dashLength, dashGap))
drawRoundRect(
color = Color(0xFFD6D6D6),
style = Stroke(strokeWidth, pathEffect = pathEffect),
cornerRadius = CornerRadius(8.dp.toPx())
)
}
.clip(RoundedCornerShape(16.dp)) // 设置圆角
.background(Color(0xFFFAF9FB)) // 设置背景色
.noRippleClickable {
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
@@ -398,7 +555,7 @@ fun AddImageGrid() {
painter = painterResource(id = R.drawable.rider_pro_camera),
contentDescription = "Take Photo",
modifier = Modifier
.size(48.dp)
.size(24.dp)
.align(Alignment.Center),
tint = Color(0xFFD6D6D6)
)

View File

@@ -8,12 +8,18 @@ import android.net.Uri
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.entity.createMultipartBody
import com.aiosman.ravenow.event.MomentAddEvent
import com.aiosman.ravenow.exp.rotate
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
@@ -21,6 +27,9 @@ import com.aiosman.ravenow.ui.modification.Modification
import com.aiosman.ravenow.utils.FileUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.io.FileOutputStream
@@ -96,6 +105,7 @@ data class Draft(
object NewPostViewModel : ViewModel() {
var momentService: MomentService = MomentServiceImpl()
var textContent by mutableStateOf("")
var aiTextContent by mutableStateOf("")
var searchPlaceAddressResult by mutableStateOf<SearchPlaceAddressResult?>(null)
var modificationList by mutableStateOf<List<Modification>>(listOf())
var imageList by mutableStateOf(listOf<ImageItem>())
@@ -111,6 +121,7 @@ object NewPostViewModel : ViewModel() {
// }
fun asNewPost() {
textContent = ""
aiTextContent = ""
searchPlaceAddressResult = null
modificationList = listOf()
imageList = listOf()
@@ -163,12 +174,17 @@ object NewPostViewModel : ViewModel() {
onUploadProgress(((index / imageList.size).toFloat())) // progressValue 是当前上传进度,例如 0.5 表示 50%
index += 1
}
aiTextContent = ""
val result = momentService.createMoment(textContent, 1, uploadImageList, relPostId)
// 刷新个人动态
MyProfileViewModel.loadProfile(pullRefresh = true)
EventBus.getDefault().post(MomentAddEvent(result))
}
suspend fun agentMoment(textContent: String,) {
aiTextContent = momentService.agentMoment(textContent)
}
suspend fun init() {
relPostId?.let {
val moment = momentService.getMomentById(it)