2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.10" />
|
<option name="version" value="2.2.21" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -41,6 +42,7 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
@@ -90,6 +96,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
|||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
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 isAiEnabled by remember { mutableStateOf(false) }
|
||||||
var isRotating by remember { mutableStateOf(false) }
|
var isRotating by remember { mutableStateOf(false) }
|
||||||
var isRequesting by remember { mutableStateOf(false) }
|
var isRequesting by remember { mutableStateOf(false) }
|
||||||
|
var buttonBottomY by remember { mutableStateOf(0f) }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val model = NewPostViewModel
|
val model = NewPostViewModel
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
@@ -110,6 +123,13 @@ fun NewPostScreen() {
|
|||||||
model.init()
|
model.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 退出时自动保存草稿
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
model.saveDraft(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
StatusBarMaskLayout(
|
StatusBarMaskLayout(
|
||||||
darkIcons = !AppState.darkMode,
|
darkIcons = !AppState.darkMode,
|
||||||
@@ -119,10 +139,14 @@ fun NewPostScreen() {
|
|||||||
AppColors.background
|
AppColors.background
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
NewPostTopBar {
|
NewPostTopBar {
|
||||||
}
|
}
|
||||||
@@ -149,8 +173,8 @@ fun NewPostScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AddImageGrid()
|
AddImageGrid()
|
||||||
NewPostTextField(stringResource(R.string.moment_content_hint), NewPostViewModel.textContent) {
|
NewPostTextField(stringResource(R.string.moment_content_hint), NewPostViewModel.textContent) { newValue ->
|
||||||
NewPostViewModel.textContent = it
|
NewPostViewModel.textContent = newValue
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -160,6 +184,7 @@ fun NewPostScreen() {
|
|||||||
.background(AppColors.divider)
|
.background(AppColors.divider)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
val isButtonEnabled = model.textContent.isNotEmpty()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
@@ -167,7 +192,12 @@ fun NewPostScreen() {
|
|||||||
.widthIn(min = 100.dp, max = 200.dp)
|
.widthIn(min = 100.dp, max = 200.dp)
|
||||||
.wrapContentWidth()
|
.wrapContentWidth()
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(
|
.onGloballyPositioned { coordinates ->
|
||||||
|
buttonBottomY = with(density) { coordinates.size.height.toDp().toPx() + coordinates.positionInRoot().y }
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (isButtonEnabled) {
|
||||||
|
Modifier.background(
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color(0xFF8CDDFF),
|
Color(0xFF8CDDFF),
|
||||||
@@ -176,6 +206,33 @@ fun NewPostScreen() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
.padding(horizontal = 14.dp, vertical = 8.dp),
|
.padding(horizontal = 14.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
@@ -284,7 +341,38 @@ fun NewPostScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部背景图
|
||||||
|
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 uploading by remember { mutableStateOf(false) }
|
||||||
var lastBackClickTime by remember { mutableStateOf(0L) }
|
var lastBackClickTime by remember { mutableStateOf(0L) }
|
||||||
var lastSendClickTime by remember { mutableStateOf(0L) }
|
var lastSendClickTime by remember { mutableStateOf(0L) }
|
||||||
|
var showDraftBox by remember { mutableStateOf(false) }
|
||||||
val debounceTime = 500L // 500毫秒防抖时间
|
val debounceTime = 500L // 500毫秒防抖时间
|
||||||
// 上传进度
|
// 上传进度
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
@@ -340,6 +429,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (currentTime - lastSendClickTime > debounceTime) {
|
if (currentTime - lastSendClickTime > debounceTime) {
|
||||||
lastSendClickTime = currentTime
|
lastSendClickTime = currentTime
|
||||||
|
// 保存草稿后再返回
|
||||||
|
model.saveDraft(context)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -363,6 +454,7 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (currentTime - lastSendClickTime > debounceTime) {
|
if (currentTime - lastSendClickTime > debounceTime) {
|
||||||
lastSendClickTime = currentTime
|
lastSendClickTime = currentTime
|
||||||
|
showDraftBox = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
@@ -386,6 +478,8 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
model.createMoment(context = context) { progress ->
|
model.createMoment(context = context) { progress ->
|
||||||
// 更新进度条
|
// 更新进度条
|
||||||
}
|
}
|
||||||
|
// 发布成功后清空内容
|
||||||
|
model.asNewPost()
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}catch (e:Exception) {
|
}catch (e:Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -398,6 +492,13 @@ fun NewPostTopBar(onSendClick: () -> Unit = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 草稿箱弹窗
|
||||||
|
if (showDraftBox) {
|
||||||
|
DraftBoxBottomSheet(
|
||||||
|
onDismiss = { showDraftBox = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.aiosman.ravenow.exp.rotate
|
|||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||||
import com.aiosman.ravenow.ui.modification.Modification
|
import com.aiosman.ravenow.ui.modification.Modification
|
||||||
|
import com.aiosman.ravenow.utils.DraftStore
|
||||||
import com.aiosman.ravenow.utils.FileUtil
|
import com.aiosman.ravenow.utils.FileUtil
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -101,7 +102,8 @@ data class DraftImageItem(
|
|||||||
}
|
}
|
||||||
data class Draft(
|
data class Draft(
|
||||||
val textContent: String,
|
val textContent: String,
|
||||||
val imageList: List<DraftImageItem>
|
val imageList: List<DraftImageItem>,
|
||||||
|
val createdAt: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
object NewPostViewModel : ViewModel() {
|
object NewPostViewModel : ViewModel() {
|
||||||
var momentService: MomentService = MomentServiceImpl()
|
var momentService: MomentService = MomentServiceImpl()
|
||||||
@@ -114,6 +116,7 @@ object NewPostViewModel : ViewModel() {
|
|||||||
var relMoment by mutableStateOf<MomentEntity?>(null)
|
var relMoment by mutableStateOf<MomentEntity?>(null)
|
||||||
var currentPhotoUri: Uri? = null
|
var currentPhotoUri: Uri? = null
|
||||||
var draft: Draft? = null
|
var draft: Draft? = null
|
||||||
|
private var draftSaved = false // 标记草稿是否已保存,避免重复保存
|
||||||
// watch textContent change and save draft
|
// watch textContent change and save draft
|
||||||
// fun saveDraft() {
|
// fun saveDraft() {
|
||||||
// draft = Draft(textContent, imageList.map {
|
// draft = Draft(textContent, imageList.map {
|
||||||
@@ -127,6 +130,7 @@ object NewPostViewModel : ViewModel() {
|
|||||||
modificationList = listOf()
|
modificationList = listOf()
|
||||||
imageList = listOf()
|
imageList = listOf()
|
||||||
relPostId = null
|
relPostId = null
|
||||||
|
draftSaved = false // 重置保存标志
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asNewPostWithImageUris(imageUris: List<String>) {
|
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="text_error_password_too_long">パスワードは%1$d文字を超えることはできません</string>
|
||||||
<string name="block">ブロック</string>
|
<string name="block">ブロック</string>
|
||||||
<string name="read_full_article">全文を読む</string>
|
<string name="read_full_article">全文を読む</string>
|
||||||
|
<string name="drafts">ドラフトボックス</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
<string name="create_title">作成</string>
|
<string name="create_title">作成</string>
|
||||||
@@ -368,5 +368,20 @@
|
|||||||
<string name="explore">探検する</string>
|
<string name="explore">探検する</string>
|
||||||
<string name="reply_to_user">返信@%1$s</string>
|
<string name="reply_to_user">返信@%1$s</string>
|
||||||
<string name="error_select_at_least_one_image">少なくとも1枚の画像を選択してください。</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>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
<string name="agent_create">创建智能体</string>
|
<string name="agent_create">创建智能体</string>
|
||||||
<string name="create_confirm">好的,就它了</string>
|
<string name="create_confirm">好的,就它了</string>
|
||||||
<string name="moment_content_hint">需要一些灵感来写文章吗?让人工智能来帮你!</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_delete">删除</string>
|
||||||
<string name="moment_ai_apply">应用</string>
|
<string name="moment_ai_apply">应用</string>
|
||||||
<string name="chat_ai">智能体</string>
|
<string name="chat_ai">智能体</string>
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<string name="chatting_now">人正在热聊…</string>
|
<string name="chatting_now">人正在热聊…</string>
|
||||||
<string name="block">拉黑</string>
|
<string name="block">拉黑</string>
|
||||||
<string name="read_full_article">查看全文</string>
|
<string name="read_full_article">查看全文</string>
|
||||||
|
<string name="drafts">草稿箱</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
<string name="create_title">创建</string>
|
<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="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
||||||
<string name="block">Block</string>
|
<string name="block">Block</string>
|
||||||
<string name="read_full_article">Read full article</string>
|
<string name="read_full_article">Read full article</string>
|
||||||
|
<string name="drafts">drafts</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
<string name="create_title">Create</string>
|
<string name="create_title">Create</string>
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Fri Jun 14 03:23:01 CST 2024
|
#Fri Jun 14 03:23:01 CST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user