BIN
app/src/main/assets/star_Loader.lottie
Normal file
BIN
app/src/main/assets/star_Loader.lottie
Normal file
Binary file not shown.
346
app/src/main/java/com/aiosman/ravenow/ui/post/DraftBox.kt
Normal file
346
app/src/main/java/com/aiosman/ravenow/ui/post/DraftBox.kt
Normal file
@@ -0,0 +1,346 @@
|
||||
package com.aiosman.ravenow.ui.post
|
||||
|
||||
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.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.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 android.graphics.BitmapFactory
|
||||
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.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.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.AppThemeData
|
||||
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.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.utils.DraftStore
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DraftBoxBottomSheet(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val model = NewPostViewModel
|
||||
|
||||
var drafts by remember { mutableStateOf<List<Draft>>(emptyList()) }
|
||||
val draftStore = remember { DraftStore(context) }
|
||||
val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
drafts = draftStore.getAllDrafts()
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = AppColors.background,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
dragHandle = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.9f)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// 标题
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.drafts),
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 草稿列表
|
||||
if (drafts.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "暂无草稿",
|
||||
fontSize = 16.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
itemsIndexed(drafts) { index, draft ->
|
||||
DraftItem(
|
||||
draft = draft,
|
||||
dateFormat = dateFormat,
|
||||
onEditClick = {
|
||||
model.viewModelScope.launch {
|
||||
model.loadDraft(context, draft)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
onDeleteClick = {
|
||||
draftStore.deleteDraft(index)
|
||||
drafts = draftStore.getAllDrafts()
|
||||
},
|
||||
AppColors = AppColors,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 底部提示
|
||||
Text(
|
||||
text = "仅保存最近5个草稿",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DraftItem(
|
||||
draft: Draft,
|
||||
dateFormat: SimpleDateFormat,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
AppColors: AppThemeData,
|
||||
context: android.content.Context
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column {
|
||||
// 文字内容(最多1行,超出用...截断)
|
||||
if (draft.textContent.isNotBlank()) {
|
||||
Text(
|
||||
text = draft.textContent,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
if (draft.imageList.isNotEmpty()) {
|
||||
val displayImages = draft.imageList.take(5) // 最多显示5张
|
||||
val totalImages = draft.imageList.size
|
||||
val showMoreIndicator = totalImages > 5
|
||||
val visibleCount = if (showMoreIndicator) 3 else displayImages.size
|
||||
val remainingCount = totalImages - 3
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 显示前3张或全部(如果少于5张)
|
||||
displayImages.take(visibleCount).forEach { imageItem ->
|
||||
DraftImageThumbnail(
|
||||
imageItem = imageItem,
|
||||
context = context,
|
||||
modifier = Modifier.size(55.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 如果超过5张,显示"more X images"
|
||||
if (showMoreIndicator) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(55.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFFFAF9FB)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "more\n$remainingCount\nimages",
|
||||
fontSize = 11.sp,
|
||||
color = AppColors.secondaryText,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 编辑和删除按钮
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 编辑按钮
|
||||
Row(
|
||||
modifier = Modifier.noRippleClickable { onEditClick() },
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_moment_apply),
|
||||
contentDescription = "edit",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = AppColors.text
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.edit_profile),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
Row(
|
||||
modifier = Modifier.noRippleClickable { onDeleteClick() },
|
||||
horizontalArrangement = Arrangement.spacedBy(4.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.delete),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 时间戳
|
||||
Text(
|
||||
text = dateFormat.format(Date(draft.createdAt)),
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DraftImageThumbnail(
|
||||
imageItem: DraftImageItem,
|
||||
context: android.content.Context,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFFFAF9FB))
|
||||
) {
|
||||
val file = File(context.cacheDir, imageItem.filename)
|
||||
if (file.exists()) {
|
||||
// 尝试从文件加载 Bitmap
|
||||
val bitmap = remember(file.absolutePath) {
|
||||
BitmapFactory.decodeFile(file.absolutePath)
|
||||
}
|
||||
if (bitmap != null) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = bitmap,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
// 如果加载失败,尝试使用 URI
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = imageItem.uri,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 如果文件不存在,显示占位符
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
|
||||
contentDescription = "image",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -41,6 +42,7 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -63,7 +65,10 @@ 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.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -72,6 +77,7 @@ 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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -90,6 +96,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
|
||||
/**
|
||||
* 发布动态
|
||||
@@ -101,7 +111,10 @@ fun NewPostScreen() {
|
||||
var isAiEnabled by remember { mutableStateOf(false) }
|
||||
var isRotating by remember { mutableStateOf(false) }
|
||||
var isRequesting by remember { mutableStateOf(false) }
|
||||
var buttonBottomY by remember { mutableStateOf(0f) }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val density = LocalDensity.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val model = NewPostViewModel
|
||||
val systemUiController = rememberSystemUiController()
|
||||
@@ -110,6 +123,13 @@ fun NewPostScreen() {
|
||||
model.init()
|
||||
}
|
||||
|
||||
// 退出时自动保存草稿
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
model.saveDraft(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StatusBarMaskLayout(
|
||||
darkIcons = !AppState.darkMode,
|
||||
@@ -119,172 +139,240 @@ fun NewPostScreen() {
|
||||
AppColors.background
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
NewPostTopBar {
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
model.relMoment?.let {
|
||||
Text("Share with")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color = AppColors.basicMain)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
RelPostCard(
|
||||
momentEntity = it,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
NewPostTopBar {
|
||||
}
|
||||
}
|
||||
|
||||
AddImageGrid()
|
||||
NewPostTextField(stringResource(R.string.moment_content_hint), NewPostViewModel.textContent) {
|
||||
NewPostViewModel.textContent = it
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(AppColors.divider)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.height(40.dp)
|
||||
.widthIn(min = 100.dp, max = 200.dp)
|
||||
.wrapContentWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF8CDDFF),
|
||||
Color(0xFF9887FF),
|
||||
Color(0xFFFF8D28)
|
||||
),
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.icon_ai),
|
||||
contentDescription = null,
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.moment_ai_co),
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp),
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
model.relMoment?.let {
|
||||
Text("Share with")
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
model.aiTextContent = ""
|
||||
}
|
||||
.background(
|
||||
color = AppColors.basicMain,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(color = AppColors.basicMain)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
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)
|
||||
RelPostCard(
|
||||
momentEntity = it,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(14.dp))
|
||||
//应用生成文案
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
if (model.aiTextContent.isNotEmpty()) {
|
||||
model.textContent = model.aiTextContent
|
||||
AddImageGrid()
|
||||
NewPostTextField(stringResource(R.string.moment_content_hint), NewPostViewModel.textContent) { newValue ->
|
||||
NewPostViewModel.textContent = newValue
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(AppColors.divider)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
val isButtonEnabled = model.textContent.isNotEmpty()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.height(40.dp)
|
||||
.widthIn(min = 100.dp, max = 200.dp)
|
||||
.wrapContentWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.onGloballyPositioned { coordinates ->
|
||||
buttonBottomY = with(density) { coordinates.size.height.toDp().toPx() + coordinates.positionInRoot().y }
|
||||
}
|
||||
.then(
|
||||
if (isButtonEnabled) {
|
||||
Modifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF8CDDFF),
|
||||
Color(0xFF9887FF),
|
||||
Color(0xFFFF8D28)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Modifier.background(Color(0xFFD4D1D6))
|
||||
}
|
||||
)
|
||||
.then(
|
||||
if (isButtonEnabled) {
|
||||
Modifier.noRippleClickable {
|
||||
model.viewModelScope.launch {
|
||||
isRequesting = true
|
||||
try {
|
||||
model.agentMoment(model.textContent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
context,
|
||||
"文案优化失败:${e.message ?: "请稍后重试"}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} finally {
|
||||
isRequesting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(
|
||||
color = AppColors.basicMain,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.icon_ai),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.moment_ai_co),
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier
|
||||
.padding(start = 2.dp),
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
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 // 靠右对齐
|
||||
) {
|
||||
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)
|
||||
)
|
||||
// 删除按钮
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部背景图
|
||||
if (isRequesting) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.component),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp)
|
||||
.align(Alignment.BottomStart)
|
||||
.offset(y = (-40).dp),
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
|
||||
// 加载动画显示在背景图上方,按钮下方80dp区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset {
|
||||
val yOffset = (buttonBottomY + with(density) { 60.dp.toPx() }).toInt()
|
||||
IntOffset(x = 0, y = yOffset)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
LottieAnimation(
|
||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
|
||||
iterations = LottieConstants.IterateForever,
|
||||
modifier = Modifier.size(200.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,6 +385,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
var uploading by remember { mutableStateOf(false) }
|
||||
var lastBackClickTime by remember { mutableStateOf(0L) }
|
||||
var lastSendClickTime by remember { mutableStateOf(0L) }
|
||||
var showDraftBox by remember { mutableStateOf(false) }
|
||||
val debounceTime = 500L // 500毫秒防抖时间
|
||||
// 上传进度
|
||||
if (uploading) {
|
||||
@@ -340,6 +429,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastSendClickTime > debounceTime) {
|
||||
lastSendClickTime = currentTime
|
||||
// 保存草稿后再返回
|
||||
model.saveDraft(context)
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
@@ -363,6 +454,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastSendClickTime > debounceTime) {
|
||||
lastSendClickTime = currentTime
|
||||
showDraftBox = true
|
||||
}
|
||||
},
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
@@ -386,6 +478,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
model.createMoment(context = context) { progress ->
|
||||
// 更新进度条
|
||||
}
|
||||
// 发布成功后清空内容
|
||||
model.asNewPost()
|
||||
navController.popBackStack()
|
||||
}catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -398,6 +492,13 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 草稿箱弹窗
|
||||
if (showDraftBox) {
|
||||
DraftBoxBottomSheet(
|
||||
onDismiss = { showDraftBox = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.aiosman.ravenow.exp.rotate
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.ravenow.ui.modification.Modification
|
||||
import com.aiosman.ravenow.utils.DraftStore
|
||||
import com.aiosman.ravenow.utils.FileUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -101,7 +102,8 @@ data class DraftImageItem(
|
||||
}
|
||||
data class Draft(
|
||||
val textContent: String,
|
||||
val imageList: List<DraftImageItem>
|
||||
val imageList: List<DraftImageItem>,
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
object NewPostViewModel : ViewModel() {
|
||||
var momentService: MomentService = MomentServiceImpl()
|
||||
@@ -114,6 +116,7 @@ object NewPostViewModel : ViewModel() {
|
||||
var relMoment by mutableStateOf<MomentEntity?>(null)
|
||||
var currentPhotoUri: Uri? = null
|
||||
var draft: Draft? = null
|
||||
private var draftSaved = false // 标记草稿是否已保存,避免重复保存
|
||||
// watch textContent change and save draft
|
||||
// fun saveDraft() {
|
||||
// draft = Draft(textContent, imageList.map {
|
||||
@@ -127,6 +130,7 @@ object NewPostViewModel : ViewModel() {
|
||||
modificationList = listOf()
|
||||
imageList = listOf()
|
||||
relPostId = null
|
||||
draftSaved = false // 重置保存标志
|
||||
}
|
||||
|
||||
fun asNewPostWithImageUris(imageUris: List<String>) {
|
||||
@@ -200,4 +204,54 @@ object NewPostViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前编辑内容为草稿
|
||||
*/
|
||||
fun saveDraft(context: Context) {
|
||||
// 如果已经保存过,不再重复保存
|
||||
if (draftSaved) {
|
||||
return
|
||||
}
|
||||
|
||||
val draftStore = DraftStore(context)
|
||||
val draft = Draft(
|
||||
textContent = textContent,
|
||||
imageList = imageList.map { DraftImageItem.fromImageItem(it) }
|
||||
)
|
||||
draftStore.saveDraft(draft)
|
||||
draftSaved = true // 标记已保存
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载草稿到编辑页面
|
||||
*/
|
||||
suspend fun loadDraft(context: Context, draft: Draft) {
|
||||
textContent = draft.textContent
|
||||
aiTextContent = ""
|
||||
draftSaved = false // 加载草稿后重置保存标志,允许重新保存
|
||||
|
||||
// 从草稿的图片URI恢复图片列表
|
||||
imageList = withContext(Dispatchers.IO) {
|
||||
draft.imageList.mapNotNull { draftImageItem ->
|
||||
try {
|
||||
val file = File(context.cacheDir, draftImageItem.filename)
|
||||
if (file.exists()) {
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
if (bitmap != null) {
|
||||
ImageItem(
|
||||
uri = draftImageItem.uri,
|
||||
id = draftImageItem.id,
|
||||
bitmap = bitmap,
|
||||
file = file
|
||||
)
|
||||
} else null
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
Log.e("NewPostViewModel", "Failed to load draft image", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
app/src/main/java/com/aiosman/ravenow/utils/DraftStore.kt
Normal file
92
app/src/main/java/com/aiosman/ravenow/utils/DraftStore.kt
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.aiosman.ravenow.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.aiosman.ravenow.ui.post.Draft
|
||||
import com.aiosman.ravenow.ui.post.DraftImageItem
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 草稿存储管理器
|
||||
* - 仅保存最近5条草稿
|
||||
* - 新草稿加入时自动淘汰最旧的一条
|
||||
*/
|
||||
class DraftStore(context: Context) {
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
data class DraftEntity(
|
||||
val textContent: String,
|
||||
val imageList: List<DraftImageItem>,
|
||||
val createdAt: Long
|
||||
) {
|
||||
fun toDraft(): Draft {
|
||||
return Draft(textContent, imageList, createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllDrafts(): List<Draft> {
|
||||
val json = prefs.getString(KEY_DRAFTS, "[]") ?: "[]"
|
||||
return runCatching {
|
||||
val type = object : TypeToken<List<DraftEntity>>() {}.type
|
||||
val entities = gson.fromJson<List<DraftEntity>>(json, type) ?: emptyList()
|
||||
entities.sortedByDescending { it.createdAt }.map { it.toDraft() }
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
fun saveDraft(draft: Draft) {
|
||||
// 如果文字和图片都为空,不保存
|
||||
if (draft.textContent.isBlank() && draft.imageList.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val current = getAllDrafts().map { draftToEntity(it) }.toMutableList()
|
||||
|
||||
// 添加新草稿到最前面
|
||||
current.add(0, draftToEntity(draft))
|
||||
|
||||
// 只保留最近5条
|
||||
while (current.size > MAX_SIZE) {
|
||||
current.removeAt(current.lastIndex)
|
||||
}
|
||||
|
||||
save(current)
|
||||
}
|
||||
|
||||
fun deleteDraft(index: Int) {
|
||||
val current = getAllDrafts().map { draftToEntity(it) }.toMutableList()
|
||||
if (index >= 0 && index < current.size) {
|
||||
current.removeAt(index)
|
||||
save(current)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
save(emptyList())
|
||||
}
|
||||
|
||||
private fun draftToEntity(draft: Draft): DraftEntity {
|
||||
return DraftEntity(
|
||||
textContent = draft.textContent,
|
||||
imageList = draft.imageList,
|
||||
createdAt = draft.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
private fun save(list: List<DraftEntity>) {
|
||||
prefs.edit().putString(KEY_DRAFTS, gson.toJson(list)).apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_NAME = "draft_store_pref"
|
||||
private const val KEY_DRAFTS = "drafts_v1"
|
||||
private const val MAX_SIZE = 5
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/mipmap-hdpi/component.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/component.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -210,7 +210,7 @@
|
||||
<string name="text_error_password_too_long">パスワードは%1$d文字を超えることはできません</string>
|
||||
<string name="block">ブロック</string>
|
||||
<string name="read_full_article">全文を読む</string>
|
||||
|
||||
<string name="drafts">ドラフトボックス</string>
|
||||
|
||||
<!-- Create Bottom Sheet -->
|
||||
<string name="create_title">作成</string>
|
||||
@@ -368,5 +368,20 @@
|
||||
<string name="explore">探検する</string>
|
||||
<string name="reply_to_user">返信@%1$s</string>
|
||||
<string name="error_select_at_least_one_image">少なくとも1枚の画像を選択してください。</string>
|
||||
|
||||
<!-- Create Group Chat Confirm -->
|
||||
<string name="create_group_chat_confirm_title">グループチャットを作成</string>
|
||||
<string name="create_group_chat_required_cost">必要消費:</string>
|
||||
<string name="create_group_chat_current_balance">現在の残高</string>
|
||||
<string name="create_group_chat_balance_after">消費後の残高:</string>
|
||||
<string name="create_group_chat_confirm_consume">消費を確認</string>
|
||||
<string name="create_group_chat_insufficient_balance">残高不足</string>
|
||||
<string name="create_group_chat_exceed_limit">メンバー数が上限を超えています(%1$d)</string>
|
||||
<string name="pai_coin">パイコイン</string>
|
||||
<string name="cost_required">必要費用</string>
|
||||
<string name="balance_after">消費後の残高</string>
|
||||
<string name="insufficient_pai_coin_balance">パイコイン残高不足</string>
|
||||
<string name="go_recharge">チャージへ</string>
|
||||
<string name="confirm_consumption">消費を確認</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<string name="agent_create">创建智能体</string>
|
||||
<string name="create_confirm">好的,就它了</string>
|
||||
<string name="moment_content_hint">需要一些灵感来写文章吗?让人工智能来帮你!</string>
|
||||
<string name="moment_ai_co">AI文案优化</string>
|
||||
<string name="moment_ai_co">文案优化</string>
|
||||
<string name="moment_ai_delete">删除</string>
|
||||
<string name="moment_ai_apply">应用</string>
|
||||
<string name="chat_ai">智能体</string>
|
||||
@@ -213,7 +213,7 @@
|
||||
<string name="chatting_now">人正在热聊…</string>
|
||||
<string name="block">拉黑</string>
|
||||
<string name="read_full_article">查看全文</string>
|
||||
|
||||
<string name="drafts">草稿箱</string>
|
||||
|
||||
<!-- Create Bottom Sheet -->
|
||||
<string name="create_title">创建</string>
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
||||
<string name="block">Block</string>
|
||||
<string name="read_full_article">Read full article</string>
|
||||
<string name="drafts">drafts</string>
|
||||
|
||||
<!-- Create Bottom Sheet -->
|
||||
<string name="create_title">Create</string>
|
||||
|
||||
Reference in New Issue
Block a user