diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index f8467b4..8ad8c86 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/assets/star_Loader.lottie b/app/src/main/assets/star_Loader.lottie
new file mode 100644
index 0000000..fb0df50
Binary files /dev/null and b/app/src/main/assets/star_Loader.lottie differ
diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/DraftBox.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/DraftBox.kt
new file mode 100644
index 0000000..c466408
--- /dev/null
+++ b/app/src/main/java/com/aiosman/ravenow/ui/post/DraftBox.kt
@@ -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>(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
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt
index 31a1379..2179b24 100644
--- a/app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt
+++ b/app/src/main/java/com/aiosman/ravenow/ui/post/NewPost.kt
@@ -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
diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/NewPostViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/NewPostViewModel.kt
index d286baf..0d17471 100644
--- a/app/src/main/java/com/aiosman/ravenow/ui/post/NewPostViewModel.kt
+++ b/app/src/main/java/com/aiosman/ravenow/ui/post/NewPostViewModel.kt
@@ -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
+ val imageList: List,
+ val createdAt: Long = System.currentTimeMillis()
)
object NewPostViewModel : ViewModel() {
var momentService: MomentService = MomentServiceImpl()
@@ -114,6 +116,7 @@ object NewPostViewModel : ViewModel() {
var relMoment by mutableStateOf(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) {
@@ -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
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aiosman/ravenow/utils/DraftStore.kt b/app/src/main/java/com/aiosman/ravenow/utils/DraftStore.kt
new file mode 100644
index 0000000..d1c01d5
--- /dev/null
+++ b/app/src/main/java/com/aiosman/ravenow/utils/DraftStore.kt
@@ -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,
+ val createdAt: Long
+ ) {
+ fun toDraft(): Draft {
+ return Draft(textContent, imageList, createdAt)
+ }
+ }
+
+ fun getAllDrafts(): List {
+ val json = prefs.getString(KEY_DRAFTS, "[]") ?: "[]"
+ return runCatching {
+ val type = object : TypeToken>() {}.type
+ val entities = gson.fromJson>(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) {
+ 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
+ }
+}
+
diff --git a/app/src/main/res/mipmap-hdpi/component.png b/app/src/main/res/mipmap-hdpi/component.png
new file mode 100644
index 0000000..67457ec
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/component.png differ
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 8237076..19f9b0d 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -210,7 +210,7 @@
パスワードは%1$d文字を超えることはできません
ブロック
全文を読む
-
+ ドラフトボックス
作成
@@ -368,5 +368,20 @@
探検する
返信@%1$s
少なくとも1枚の画像を選択してください。
+
+
+ グループチャットを作成
+ 必要消費:
+ 現在の残高
+ 消費後の残高:
+ 消費を確認
+ 残高不足
+ メンバー数が上限を超えています(%1$d)
+ パイコイン
+ 必要費用
+ 消費後の残高
+ パイコイン残高不足
+ チャージへ
+ 消費を確認
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 77d2a57..5907627 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -149,7 +149,7 @@
创建智能体
好的,就它了
需要一些灵感来写文章吗?让人工智能来帮你!
- AI文案优化
+ 文案优化
删除
应用
智能体
@@ -213,7 +213,7 @@
人正在热聊…
拉黑
查看全文
-
+ 草稿箱
创建
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9fb066a..eadf2ba 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -209,6 +209,7 @@
Password cannot exceed %1$d characters
Block
Read full article
+ drafts
Create
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 8635977..60568db 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Fri Jun 14 03:23:01 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists