发布动态页面调整

This commit is contained in:
2025-11-13 18:55:05 +08:00
parent c5e6843b35
commit 238b7dfb75
11 changed files with 759 additions and 150 deletions

Binary file not shown.

View 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
)
}
}
}
}

View File

@@ -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

View File

@@ -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
}
}
}
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>