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