推文图片支持九宫格&所有图片并发上传并显示上传进度
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
|||||||
applicationId = "com.memory.app"
|
applicationId = "com.memory.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 34
|
versionCode = 41
|
||||||
versionName = "1.5.0"
|
versionName = "1.5.7"
|
||||||
|
|
||||||
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
||||||
buildConfigField("int", "VERSION_CODE", "34")
|
buildConfigField("int", "VERSION_CODE", "41")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.memory.app.data.api
|
||||||
|
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.ForwardingSink
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
|
class ProgressRequestBody(
|
||||||
|
private val delegate: RequestBody,
|
||||||
|
private val onProgress: (Float) -> Unit
|
||||||
|
) : RequestBody() {
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? = delegate.contentType()
|
||||||
|
|
||||||
|
override fun contentLength(): Long = delegate.contentLength()
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
val total = contentLength()
|
||||||
|
if (total <= 0) {
|
||||||
|
delegate.writeTo(sink)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressSink = object : ForwardingSink(sink) {
|
||||||
|
private var uploaded = 0L
|
||||||
|
|
||||||
|
override fun write(source: Buffer, byteCount: Long) {
|
||||||
|
super.write(source, byteCount)
|
||||||
|
uploaded += byteCount
|
||||||
|
onProgress((uploaded.toFloat() / total).coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bufferedSink = progressSink.buffer()
|
||||||
|
delegate.writeTo(bufferedSink)
|
||||||
|
bufferedSink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,32 +95,39 @@ fun MediaGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// 4+ images: 2x2 grid
|
// 4+ images: 3x3 grid (九宫格)
|
||||||
|
val columns = if (media.size == 4) 2 else 3
|
||||||
|
val rows = (media.size + columns - 1) / columns
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
|
||||||
.clip(shape),
|
.clip(shape),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
for (row in 0..1) {
|
for (row in 0 until rows) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(columns.toFloat()),
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
for (col in 0..1) {
|
for (col in 0 until columns) {
|
||||||
val index = row * 2 + col
|
val index = row * columns + col
|
||||||
if (index < media.size) {
|
if (index < media.size) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = media[index].mediaUrl,
|
model = media[index].mediaUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.aspectRatio(1f)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.clickable(enabled = onMediaClick != null) { onMediaClick?.invoke(index) },
|
.clickable(enabled = onMediaClick != null) { onMediaClick?.invoke(index) },
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// 占位,保持布局
|
||||||
|
Spacer(modifier = Modifier.weight(1f).aspectRatio(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ fun MainNavigation(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isPosting by homeViewModel.isPosting.collectAsState()
|
val isPosting by homeViewModel.isPosting.collectAsState()
|
||||||
val postSuccess by homeViewModel.postSuccess.collectAsState()
|
val postSuccess by homeViewModel.postSuccess.collectAsState()
|
||||||
|
val editSuccess by homeViewModel.editSuccess.collectAsState()
|
||||||
|
|
||||||
val profileUser by profileViewModel.user.collectAsState()
|
val profileUser by profileViewModel.user.collectAsState()
|
||||||
val profilePostCount by profileViewModel.postCount.collectAsState()
|
val profilePostCount by profileViewModel.postCount.collectAsState()
|
||||||
@@ -70,6 +71,7 @@ fun MainNavigation(
|
|||||||
val unreadCount by notificationViewModel.unreadCount.collectAsState()
|
val unreadCount by notificationViewModel.unreadCount.collectAsState()
|
||||||
val notifications by notificationViewModel.notifications.collectAsState()
|
val notifications by notificationViewModel.notifications.collectAsState()
|
||||||
val notificationLoading by notificationViewModel.isLoading.collectAsState()
|
val notificationLoading by notificationViewModel.isLoading.collectAsState()
|
||||||
|
val uploadProgress by homeViewModel.uploadProgress.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(user) {
|
LaunchedEffect(user) {
|
||||||
profileViewModel.setUser(user)
|
profileViewModel.setUser(user)
|
||||||
@@ -84,6 +86,13 @@ fun MainNavigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(editSuccess) {
|
||||||
|
if (editSuccess) {
|
||||||
|
editingPost = null
|
||||||
|
homeViewModel.resetEditSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -265,7 +274,8 @@ fun MainNavigation(
|
|||||||
onPost = { content, images, visibility, musicUrl ->
|
onPost = { content, images, visibility, musicUrl ->
|
||||||
homeViewModel.createPost(context, content, images, visibility, musicUrl)
|
homeViewModel.createPost(context, content, images, visibility, musicUrl)
|
||||||
},
|
},
|
||||||
isLoading = isPosting
|
isLoading = isPosting,
|
||||||
|
uploadProgress = uploadProgress
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +292,10 @@ fun MainNavigation(
|
|||||||
// 更新文字、图片、可见性和音乐
|
// 更新文字、图片、可见性和音乐
|
||||||
homeViewModel.updatePostWithImages(context, post.id, newContent, existingUrls, newImages, visibility, musicUrl)
|
homeViewModel.updatePostWithImages(context, post.id, newContent, existingUrls, newImages, visibility, musicUrl)
|
||||||
}
|
}
|
||||||
editingPost = null
|
// 不在这里关闭,等 editSuccess 触发后再关闭
|
||||||
},
|
},
|
||||||
isLoading = isPosting
|
isLoading = isPosting,
|
||||||
|
uploadProgress = uploadProgress
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package com.memory.app.ui.screen
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.MusicNote
|
import androidx.compose.material.icons.filled.MusicNote
|
||||||
import androidx.compose.material.icons.outlined.Image
|
import androidx.compose.material.icons.outlined.Image
|
||||||
@@ -42,7 +46,8 @@ fun CreatePostScreen(
|
|||||||
user: User?,
|
user: User?,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onPost: (String, List<Uri>, Int, String?) -> Unit, // content, images, visibility, musicUrl
|
onPost: (String, List<Uri>, Int, String?) -> Unit, // content, images, visibility, musicUrl
|
||||||
isLoading: Boolean
|
isLoading: Boolean,
|
||||||
|
uploadProgress: Map<Int, Float> = emptyMap()
|
||||||
) {
|
) {
|
||||||
// 拦截系统返回手势
|
// 拦截系统返回手势
|
||||||
BackHandler(enabled = true) {
|
BackHandler(enabled = true) {
|
||||||
@@ -59,10 +64,13 @@ fun CreatePostScreen(
|
|||||||
var showMusicInput by remember { mutableStateOf(false) }
|
var showMusicInput by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 计算还能选多少张
|
||||||
|
val maxSelectable = (9 - selectedImages.size).coerceAtLeast(0)
|
||||||
|
|
||||||
val imagePicker = rememberLauncherForActivityResult(
|
val imagePicker = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetMultipleContents()
|
contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 9)
|
||||||
) { uris ->
|
) { uris ->
|
||||||
selectedImages = (selectedImages + uris).take(6)
|
selectedImages = (selectedImages + uris).take(9)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -126,12 +134,13 @@ fun CreatePostScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content Area
|
// Content Area - 可滚动
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// Avatar
|
// Avatar
|
||||||
Box(
|
Box(
|
||||||
@@ -188,36 +197,26 @@ fun CreatePostScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Selected Images - 超过3张显示为两行
|
// Selected Images - 九宫格布局
|
||||||
if (selectedImages.isNotEmpty()) {
|
if (selectedImages.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
if (selectedImages.size <= 3) {
|
val columns = if (selectedImages.size <= 3) selectedImages.size else 3
|
||||||
// 3张及以下:单行显示
|
val rows = (selectedImages.size + columns - 1) / columns
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
selectedImages.forEach { uri ->
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
SelectedImageItem(
|
for (row in 0 until rows) {
|
||||||
uri = uri,
|
|
||||||
onRemove = { selectedImages = selectedImages - uri }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 超过3张:两行显示,每行3张
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
selectedImages.take(3).forEach { uri ->
|
for (col in 0 until columns) {
|
||||||
SelectedImageItem(
|
val index = row * columns + col
|
||||||
uri = uri,
|
if (index < selectedImages.size) {
|
||||||
onRemove = { selectedImages = selectedImages - uri }
|
val uri = selectedImages[index]
|
||||||
)
|
val progress = uploadProgress[index]
|
||||||
}
|
SelectedImageItem(
|
||||||
}
|
uri = uri,
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
onRemove = if (!isLoading) {{ selectedImages = selectedImages - uri }} else null,
|
||||||
selectedImages.drop(3).forEach { uri ->
|
uploadProgress = progress
|
||||||
SelectedImageItem(
|
)
|
||||||
uri = uri,
|
}
|
||||||
onRemove = { selectedImages = selectedImages - uri }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,14 +298,18 @@ fun CreatePostScreen(
|
|||||||
) {
|
) {
|
||||||
// Image Button
|
// Image Button
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { imagePicker.launch("image/*") },
|
onClick = {
|
||||||
enabled = selectedImages.size < 6,
|
imagePicker.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = selectedImages.size < 9,
|
||||||
modifier = Modifier.size(40.dp)
|
modifier = Modifier.size(40.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Image,
|
Icons.Outlined.Image,
|
||||||
contentDescription = "添加图片",
|
contentDescription = "添加图片",
|
||||||
tint = if (selectedImages.size < 6) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = if (selectedImages.size < 9) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -438,7 +441,8 @@ fun CreatePostScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SelectedImageItem(
|
private fun SelectedImageItem(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
onRemove: () -> Unit
|
onRemove: (() -> Unit)?,
|
||||||
|
uploadProgress: Float? = null
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
@@ -449,22 +453,65 @@ private fun SelectedImageItem(
|
|||||||
.clip(RoundedCornerShape(10.dp)),
|
.clip(RoundedCornerShape(10.dp)),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
// 上传进度遮罩
|
||||||
.align(Alignment.TopEnd)
|
if (uploadProgress != null && uploadProgress < 1f) {
|
||||||
.padding(4.dp)
|
Box(
|
||||||
.size(22.dp)
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.size(90.dp)
|
||||||
.background(Color.Black.copy(alpha = 0.6f))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.clickable { onRemove() },
|
.background(Color.Black.copy(alpha = 0.5f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
CircularProgressIndicator(
|
||||||
Icons.Default.Close,
|
progress = { uploadProgress },
|
||||||
contentDescription = "移除",
|
modifier = Modifier.size(36.dp),
|
||||||
modifier = Modifier.size(12.dp),
|
color = Color.White,
|
||||||
tint = Color.White
|
strokeWidth = 3.dp,
|
||||||
)
|
trackColor = Color.White.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除按钮(上传中不显示)
|
||||||
|
if (onRemove != null && uploadProgress == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(22.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.clickable { onRemove() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = "移除",
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传完成标记
|
||||||
|
if (uploadProgress != null && uploadProgress >= 1f) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(22.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Brand500),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = "完成",
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package com.memory.app.ui.screen
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.MusicNote
|
import androidx.compose.material.icons.filled.MusicNote
|
||||||
import androidx.compose.material.icons.outlined.Image
|
import androidx.compose.material.icons.outlined.Image
|
||||||
@@ -42,7 +46,8 @@ fun EditPostScreen(
|
|||||||
post: Post,
|
post: Post,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onSave: (String, List<String>, List<Uri>, Int, String?) -> Unit, // content, existingImages, newImages, visibility, musicUrl
|
onSave: (String, List<String>, List<Uri>, Int, String?) -> Unit, // content, existingImages, newImages, visibility, musicUrl
|
||||||
isLoading: Boolean
|
isLoading: Boolean,
|
||||||
|
uploadProgress: Map<Int, Float> = emptyMap()
|
||||||
) {
|
) {
|
||||||
// 拦截系统返回手势
|
// 拦截系统返回手势
|
||||||
BackHandler(enabled = true) {
|
BackHandler(enabled = true) {
|
||||||
@@ -65,9 +70,9 @@ fun EditPostScreen(
|
|||||||
val totalImages = existingImages.size + newImages.size
|
val totalImages = existingImages.size + newImages.size
|
||||||
|
|
||||||
val imagePicker = rememberLauncherForActivityResult(
|
val imagePicker = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetMultipleContents()
|
contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 9)
|
||||||
) { uris ->
|
) { uris ->
|
||||||
val remaining = 6 - totalImages
|
val remaining = 9 - totalImages
|
||||||
newImages = (newImages + uris).take(remaining.coerceAtLeast(0))
|
newImages = (newImages + uris).take(remaining.coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +138,13 @@ fun EditPostScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content Area
|
// Content Area - 可滚动
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 20.dp, vertical = 8.dp)
|
.padding(horizontal = 20.dp, vertical = 8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -187,52 +193,35 @@ fun EditPostScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Images - 合并已有图片和新图片,超过3张显示为两行
|
// Images - 合并已有图片和新图片,九宫格布局
|
||||||
val allImages = existingImages.map { ImageItem.Existing(it) } + newImages.map { ImageItem.New(it) }
|
val allImages = existingImages.map { ImageItem.Existing(it) } + newImages.mapIndexed { idx, uri -> ImageItem.New(uri, idx) }
|
||||||
if (allImages.isNotEmpty()) {
|
if (allImages.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
if (allImages.size <= 3) {
|
val columns = if (allImages.size <= 3) allImages.size else 3
|
||||||
// 3张及以下:单行显示
|
val rows = (allImages.size + columns - 1) / columns
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
allImages.forEach { item ->
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
EditImageItem(
|
for (row in 0 until rows) {
|
||||||
item = item,
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
onRemove = {
|
for (col in 0 until columns) {
|
||||||
when (item) {
|
val index = row * columns + col
|
||||||
is ImageItem.Existing -> existingImages = existingImages - item.url
|
if (index < allImages.size) {
|
||||||
is ImageItem.New -> newImages = newImages - item.uri
|
val item = allImages[index]
|
||||||
|
val progress = when (item) {
|
||||||
|
is ImageItem.New -> uploadProgress[item.index]
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
EditImageItem(
|
||||||
|
item = item,
|
||||||
|
onRemove = if (!isLoading) {{
|
||||||
|
when (item) {
|
||||||
|
is ImageItem.Existing -> existingImages = existingImages - item.url
|
||||||
|
is ImageItem.New -> newImages = newImages - item.uri
|
||||||
|
}
|
||||||
|
}} else null,
|
||||||
|
uploadProgress = progress
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 超过3张:两行显示,每行3张
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
allImages.take(3).forEach { item ->
|
|
||||||
EditImageItem(
|
|
||||||
item = item,
|
|
||||||
onRemove = {
|
|
||||||
when (item) {
|
|
||||||
is ImageItem.Existing -> existingImages = existingImages - item.url
|
|
||||||
is ImageItem.New -> newImages = newImages - item.uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
allImages.drop(3).forEach { item ->
|
|
||||||
EditImageItem(
|
|
||||||
item = item,
|
|
||||||
onRemove = {
|
|
||||||
when (item) {
|
|
||||||
is ImageItem.Existing -> existingImages = existingImages - item.url
|
|
||||||
is ImageItem.New -> newImages = newImages - item.uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,14 +298,18 @@ fun EditPostScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { imagePicker.launch("image/*") },
|
onClick = {
|
||||||
enabled = totalImages < 6,
|
imagePicker.launch(
|
||||||
|
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = totalImages < 9,
|
||||||
modifier = Modifier.size(40.dp)
|
modifier = Modifier.size(40.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Image,
|
Icons.Outlined.Image,
|
||||||
contentDescription = "添加图片",
|
contentDescription = "添加图片",
|
||||||
tint = if (totalImages < 6) Brand500 else Slate300,
|
tint = if (totalImages < 9) Brand500 else Slate300,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -426,13 +419,14 @@ fun EditPostScreen(
|
|||||||
// 图片项类型
|
// 图片项类型
|
||||||
private sealed class ImageItem {
|
private sealed class ImageItem {
|
||||||
data class Existing(val url: String) : ImageItem()
|
data class Existing(val url: String) : ImageItem()
|
||||||
data class New(val uri: Uri) : ImageItem()
|
data class New(val uri: Uri, val index: Int) : ImageItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditImageItem(
|
private fun EditImageItem(
|
||||||
item: ImageItem,
|
item: ImageItem,
|
||||||
onRemove: () -> Unit
|
onRemove: (() -> Unit)?,
|
||||||
|
uploadProgress: Float? = null
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
@@ -446,22 +440,65 @@ private fun EditImageItem(
|
|||||||
.clip(RoundedCornerShape(10.dp)),
|
.clip(RoundedCornerShape(10.dp)),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
// 上传进度遮罩
|
||||||
.align(Alignment.TopEnd)
|
if (uploadProgress != null && uploadProgress < 1f) {
|
||||||
.padding(4.dp)
|
Box(
|
||||||
.size(22.dp)
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.size(90.dp)
|
||||||
.background(Color.Black.copy(alpha = 0.6f))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.clickable { onRemove() },
|
.background(Color.Black.copy(alpha = 0.5f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
CircularProgressIndicator(
|
||||||
Icons.Default.Close,
|
progress = { uploadProgress },
|
||||||
contentDescription = "移除",
|
modifier = Modifier.size(36.dp),
|
||||||
modifier = Modifier.size(12.dp),
|
color = Color.White,
|
||||||
tint = Color.White
|
strokeWidth = 3.dp,
|
||||||
)
|
trackColor = Color.White.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除按钮(上传中不显示)
|
||||||
|
if (onRemove != null && uploadProgress == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(22.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.clickable { onRemove() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = "移除",
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传完成标记
|
||||||
|
if (uploadProgress != null && uploadProgress >= 1f) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(22.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Brand500),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = "完成",
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.memory.app.data.model.Comment
|
|||||||
import com.memory.app.data.model.Post
|
import com.memory.app.data.model.Post
|
||||||
import com.memory.app.ui.components.EmojiPickerDialog
|
import com.memory.app.ui.components.EmojiPickerDialog
|
||||||
import com.memory.app.ui.components.FullScreenImageViewer
|
import com.memory.app.ui.components.FullScreenImageViewer
|
||||||
|
import com.memory.app.ui.components.MediaGrid
|
||||||
import com.memory.app.ui.components.MusicCard
|
import com.memory.app.ui.components.MusicCard
|
||||||
import com.memory.app.ui.theme.*
|
import com.memory.app.ui.theme.*
|
||||||
import com.memory.app.util.TimeUtils
|
import com.memory.app.util.TimeUtils
|
||||||
@@ -188,21 +189,13 @@ fun PostDetailScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
// Media
|
// Media - 使用 MediaGrid 展示缩略图
|
||||||
if (post.media.isNotEmpty()) {
|
if (post.media.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
post.media.forEachIndexed { index, media ->
|
MediaGrid(
|
||||||
AsyncImage(
|
media = post.media,
|
||||||
model = media.mediaUrl,
|
onMediaClick = { index -> showImageViewerIndex = index }
|
||||||
contentDescription = null,
|
)
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.clickable { showImageViewerIndex = index },
|
|
||||||
contentScale = ContentScale.FillWidth
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Music Card
|
// Music Card
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import com.memory.app.data.model.CreatePostRequest
|
|||||||
import com.memory.app.data.model.Post
|
import com.memory.app.data.model.Post
|
||||||
import com.memory.app.data.model.UpdatePostRequest
|
import com.memory.app.data.model.UpdatePostRequest
|
||||||
import com.memory.app.data.model.User
|
import com.memory.app.data.model.User
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import com.memory.app.data.api.ProgressRequestBody
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
private val _posts = MutableStateFlow<List<Post>>(emptyList())
|
private val _posts = MutableStateFlow<List<Post>>(emptyList())
|
||||||
@@ -30,9 +33,16 @@ class HomeViewModel : ViewModel() {
|
|||||||
private val _postSuccess = MutableStateFlow(false)
|
private val _postSuccess = MutableStateFlow(false)
|
||||||
val postSuccess: StateFlow<Boolean> = _postSuccess
|
val postSuccess: StateFlow<Boolean> = _postSuccess
|
||||||
|
|
||||||
|
private val _editSuccess = MutableStateFlow(false)
|
||||||
|
val editSuccess: StateFlow<Boolean> = _editSuccess
|
||||||
|
|
||||||
private val _currentUser = MutableStateFlow<User?>(null)
|
private val _currentUser = MutableStateFlow<User?>(null)
|
||||||
val currentUser: StateFlow<User?> = _currentUser
|
val currentUser: StateFlow<User?> = _currentUser
|
||||||
|
|
||||||
|
// 上传进度: Map<图片索引, 进度(0.0~1.0)>
|
||||||
|
private val _uploadProgress = MutableStateFlow<Map<Int, Float>>(emptyMap())
|
||||||
|
val uploadProgress: StateFlow<Map<Int, Float>> = _uploadProgress
|
||||||
|
|
||||||
private var currentPage = 1
|
private var currentPage = 1
|
||||||
private var hasMore = true
|
private var hasMore = true
|
||||||
|
|
||||||
@@ -137,14 +147,17 @@ class HomeViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun updatePost(postId: Long, content: String, mediaIds: List<String>? = null, visibility: Int? = null, musicUrl: String? = null) {
|
fun updatePost(postId: Long, content: String, mediaIds: List<String>? = null, visibility: Int? = null, musicUrl: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
_isPosting.value = true
|
||||||
try {
|
try {
|
||||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, mediaIds, visibility, musicUrl))
|
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, mediaIds, visibility, musicUrl))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
// 刷新获取最新数据
|
_editSuccess.value = true
|
||||||
loadPosts()
|
loadPosts()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
_isPosting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,36 +165,61 @@ class HomeViewModel : ViewModel() {
|
|||||||
fun updatePostWithImages(context: Context, postId: Long, content: String, existingUrls: List<String>, newImages: List<Uri>, visibility: Int? = null, musicUrl: String? = null) {
|
fun updatePostWithImages(context: Context, postId: Long, content: String, existingUrls: List<String>, newImages: List<Uri>, visibility: Int? = null, musicUrl: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isPosting.value = true
|
_isPosting.value = true
|
||||||
|
_uploadProgress.value = emptyMap()
|
||||||
try {
|
try {
|
||||||
// 上传新图片
|
// 并发上传新图片(带进度)
|
||||||
val newImageUrls = mutableListOf<String>()
|
val uploadJobs = newImages.mapIndexed { index, uri ->
|
||||||
for (uri in newImages) {
|
async {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
try {
|
||||||
val bytes = inputStream?.readBytes() ?: continue
|
// 初始化进度
|
||||||
inputStream.close()
|
_uploadProgress.value = _uploadProgress.value + (index to 0f)
|
||||||
|
|
||||||
val fileName = "image_${System.currentTimeMillis()}.jpg"
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
|
val bytes = inputStream?.readBytes() ?: return@async null
|
||||||
val part = MultipartBody.Part.createFormData("file", fileName, requestBody)
|
inputStream.close()
|
||||||
|
|
||||||
val uploadResponse = ApiClient.api.upload(part)
|
val fileName = "image_${System.currentTimeMillis()}_$index.jpg"
|
||||||
if (uploadResponse.isSuccessful) {
|
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
|
||||||
uploadResponse.body()?.url?.let { newImageUrls.add(it) }
|
|
||||||
|
// 包装成带进度的 RequestBody
|
||||||
|
val progressBody = ProgressRequestBody(requestBody) { progress ->
|
||||||
|
_uploadProgress.value = _uploadProgress.value + (index to progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
val part = MultipartBody.Part.createFormData("file", fileName, progressBody)
|
||||||
|
|
||||||
|
val uploadResponse = ApiClient.api.upload(part)
|
||||||
|
if (uploadResponse.isSuccessful) {
|
||||||
|
_uploadProgress.value = _uploadProgress.value + (index to 1f)
|
||||||
|
index to uploadResponse.body()?.url
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待所有上传完成,按原始顺序排列
|
||||||
|
val newImageUrls = uploadJobs.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.mapNotNull { it.second }
|
||||||
|
|
||||||
// 合并现有图片和新图片
|
// 合并现有图片和新图片
|
||||||
val allMediaIds = existingUrls + newImageUrls
|
val allMediaIds = existingUrls + newImageUrls
|
||||||
|
|
||||||
// 更新帖子
|
// 更新帖子
|
||||||
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, allMediaIds, visibility, musicUrl))
|
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, allMediaIds, visibility, musicUrl))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
_editSuccess.value = true
|
||||||
loadPosts()
|
loadPosts()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
_isPosting.value = false
|
_isPosting.value = false
|
||||||
|
_uploadProgress.value = emptyMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,24 +233,48 @@ class HomeViewModel : ViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isPosting.value = true
|
_isPosting.value = true
|
||||||
_postSuccess.value = false
|
_postSuccess.value = false
|
||||||
|
_uploadProgress.value = emptyMap()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先上传图片
|
// 并发上传图片
|
||||||
val imageUrls = mutableListOf<String>()
|
val uploadJobs = images.mapIndexed { index, uri ->
|
||||||
for (uri in images) {
|
async {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
try {
|
||||||
val bytes = inputStream?.readBytes() ?: continue
|
// 初始化进度
|
||||||
inputStream.close()
|
_uploadProgress.value = _uploadProgress.value + (index to 0f)
|
||||||
|
|
||||||
val fileName = "image_${System.currentTimeMillis()}.jpg"
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
|
val bytes = inputStream?.readBytes() ?: return@async null
|
||||||
val part = MultipartBody.Part.createFormData("file", fileName, requestBody)
|
inputStream.close()
|
||||||
|
|
||||||
val uploadResponse = ApiClient.api.upload(part)
|
val fileName = "image_${System.currentTimeMillis()}_$index.jpg"
|
||||||
if (uploadResponse.isSuccessful) {
|
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
|
||||||
uploadResponse.body()?.url?.let { imageUrls.add(it) }
|
|
||||||
|
// 包装成带进度的 RequestBody
|
||||||
|
val progressBody = ProgressRequestBody(requestBody) { progress ->
|
||||||
|
_uploadProgress.value = _uploadProgress.value + (index to progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
val part = MultipartBody.Part.createFormData("file", fileName, progressBody)
|
||||||
|
|
||||||
|
val uploadResponse = ApiClient.api.upload(part)
|
||||||
|
if (uploadResponse.isSuccessful) {
|
||||||
|
_uploadProgress.value = _uploadProgress.value + (index to 1f)
|
||||||
|
index to uploadResponse.body()?.url
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待所有上传完成,按原始顺序排列
|
||||||
|
val imageUrls = uploadJobs.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.mapNotNull { it.second }
|
||||||
|
|
||||||
// 创建帖子
|
// 创建帖子
|
||||||
val request = CreatePostRequest(
|
val request = CreatePostRequest(
|
||||||
content = content,
|
content = content,
|
||||||
@@ -229,6 +291,7 @@ class HomeViewModel : ViewModel() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
_isPosting.value = false
|
_isPosting.value = false
|
||||||
|
_uploadProgress.value = emptyMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,4 +299,8 @@ class HomeViewModel : ViewModel() {
|
|||||||
fun resetPostSuccess() {
|
fun resetPostSuccess() {
|
||||||
_postSuccess.value = false
|
_postSuccess.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetEditSuccess() {
|
||||||
|
_editSuccess.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
release.sh
56
release.sh
@@ -263,38 +263,32 @@ if [ "$BUILD_APK" = true ]; then
|
|||||||
echo " 版本: v${new_version} (code: ${new_code})"
|
echo " 版本: v${new_version} (code: ${new_code})"
|
||||||
echo " 更新日志: ${update_log}"
|
echo " 更新日志: ${update_log}"
|
||||||
echo ""
|
echo ""
|
||||||
read -p "确认发布 APK? (y/n) " confirm
|
|
||||||
if [ "$confirm" != "y" ]; then
|
# 更新版本号
|
||||||
echo "❌ APK 发布已取消"
|
echo "📝 更新版本号..."
|
||||||
BUILD_APK=false
|
update_version_in_gradle $new_version $new_code
|
||||||
else
|
|
||||||
# 更新版本号
|
# 编译
|
||||||
|
build_apk
|
||||||
|
|
||||||
|
# 先更新服务器版本信息,成功后再上传 APK
|
||||||
|
download_url="${R2_PUBLIC_URL}/releases/memory-${new_version}.apk"
|
||||||
|
if update_server_version $new_code "$new_version" "$download_url" "$update_log"; then
|
||||||
|
# 上传 APK
|
||||||
|
upload_to_r2 $new_version
|
||||||
|
echo "✅ 上传完成: $download_url"
|
||||||
|
|
||||||
|
# 清理旧版本
|
||||||
|
cleanup_old_apks
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📝 更新版本号..."
|
echo "🎉 APK 发布完成!"
|
||||||
update_version_in_gradle $new_version $new_code
|
echo " 版本: v${new_version}"
|
||||||
|
echo " 下载: ${download_url}"
|
||||||
# 编译
|
else
|
||||||
build_apk
|
echo ""
|
||||||
|
echo "❌ 服务器版本更新失败,APK 未上传"
|
||||||
# 先更新服务器版本信息,成功后再上传 APK
|
exit 1
|
||||||
download_url="${R2_PUBLIC_URL}/releases/memory-${new_version}.apk"
|
|
||||||
if update_server_version $new_code "$new_version" "$download_url" "$update_log"; then
|
|
||||||
# 上传 APK
|
|
||||||
upload_to_r2 $new_version
|
|
||||||
echo "✅ 上传完成: $download_url"
|
|
||||||
|
|
||||||
# 清理旧版本
|
|
||||||
cleanup_old_apks
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "🎉 APK 发布完成!"
|
|
||||||
echo " 版本: v${new_version}"
|
|
||||||
echo " 下载: ${download_url}"
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "❌ 服务器版本更新失败,APK 未上传"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user