推文图片支持九宫格&所有图片并发上传并显示上传进度

This commit is contained in:
amos wong
2026-01-01 01:04:52 +08:00
parent d51e907cee
commit 44c1824269
9 changed files with 400 additions and 204 deletions

View File

@@ -13,11 +13,11 @@ android {
applicationId = "com.memory.app"
minSdk = 26
targetSdk = 35
versionCode = 34
versionName = "1.5.0"
versionCode = 41
versionName = "1.5.7"
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
buildConfigField("int", "VERSION_CODE", "34")
buildConfigField("int", "VERSION_CODE", "41")
}
signingConfigs {

View File

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

View File

@@ -95,32 +95,39 @@ fun MediaGrid(
}
}
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(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(shape),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
for (row in 0..1) {
for (row in 0 until rows) {
Row(
modifier = Modifier.weight(1f),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(columns.toFloat()),
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
for (col in 0..1) {
val index = row * 2 + col
for (col in 0 until columns) {
val index = row * columns + col
if (index < media.size) {
AsyncImage(
model = media[index].mediaUrl,
contentDescription = null,
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.aspectRatio(1f)
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable(enabled = onMediaClick != null) { onMediaClick?.invoke(index) },
contentScale = ContentScale.Crop
)
} else {
// 占位,保持布局
Spacer(modifier = Modifier.weight(1f).aspectRatio(1f))
}
}
}

View File

@@ -62,6 +62,7 @@ fun MainNavigation(
val context = LocalContext.current
val isPosting by homeViewModel.isPosting.collectAsState()
val postSuccess by homeViewModel.postSuccess.collectAsState()
val editSuccess by homeViewModel.editSuccess.collectAsState()
val profileUser by profileViewModel.user.collectAsState()
val profilePostCount by profileViewModel.postCount.collectAsState()
@@ -70,6 +71,7 @@ fun MainNavigation(
val unreadCount by notificationViewModel.unreadCount.collectAsState()
val notifications by notificationViewModel.notifications.collectAsState()
val notificationLoading by notificationViewModel.isLoading.collectAsState()
val uploadProgress by homeViewModel.uploadProgress.collectAsState()
LaunchedEffect(user) {
profileViewModel.setUser(user)
@@ -84,6 +86,13 @@ fun MainNavigation(
}
}
LaunchedEffect(editSuccess) {
if (editSuccess) {
editingPost = null
homeViewModel.resetEditSuccess()
}
}
Box(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
@@ -265,7 +274,8 @@ fun MainNavigation(
onPost = { 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)
}
editingPost = null
// 不在这里关闭,等 editSuccess 触发后再关闭
},
isLoading = isPosting
isLoading = isPosting,
uploadProgress = uploadProgress
)
}
}

View File

@@ -3,14 +3,18 @@ package com.memory.app.ui.screen
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.RoundedCornerShape
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.MusicNote
import androidx.compose.material.icons.outlined.Image
@@ -42,7 +46,8 @@ fun CreatePostScreen(
user: User?,
onClose: () -> Unit,
onPost: (String, List<Uri>, Int, String?) -> Unit, // content, images, visibility, musicUrl
isLoading: Boolean
isLoading: Boolean,
uploadProgress: Map<Int, Float> = emptyMap()
) {
// 拦截系统返回手势
BackHandler(enabled = true) {
@@ -59,10 +64,13 @@ fun CreatePostScreen(
var showMusicInput by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
// 计算还能选多少张
val maxSelectable = (9 - selectedImages.size).coerceAtLeast(0)
val imagePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 9)
) { uris ->
selectedImages = (selectedImages + uris).take(6)
selectedImages = (selectedImages + uris).take(9)
}
Column(
@@ -126,12 +134,13 @@ fun CreatePostScreen(
}
}
// Content Area
// Content Area - 可滚动
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 20.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState())
) {
// Avatar
Box(
@@ -188,36 +197,26 @@ fun CreatePostScreen(
)
)
// Selected Images - 超过3张显示为两行
// Selected Images - 九宫格布局
if (selectedImages.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
if (selectedImages.size <= 3) {
// 3张及以下单行显示
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
selectedImages.forEach { uri ->
SelectedImageItem(
uri = uri,
onRemove = { selectedImages = selectedImages - uri }
)
}
}
} else {
// 超过3张两行显示每行3张
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
val columns = if (selectedImages.size <= 3) selectedImages.size else 3
val rows = (selectedImages.size + columns - 1) / columns
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
for (row in 0 until rows) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
selectedImages.take(3).forEach { uri ->
SelectedImageItem(
uri = uri,
onRemove = { selectedImages = selectedImages - uri }
)
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
selectedImages.drop(3).forEach { uri ->
SelectedImageItem(
uri = uri,
onRemove = { selectedImages = selectedImages - uri }
)
for (col in 0 until columns) {
val index = row * columns + col
if (index < selectedImages.size) {
val uri = selectedImages[index]
val progress = uploadProgress[index]
SelectedImageItem(
uri = uri,
onRemove = if (!isLoading) {{ selectedImages = selectedImages - uri }} else null,
uploadProgress = progress
)
}
}
}
}
@@ -299,14 +298,18 @@ fun CreatePostScreen(
) {
// Image Button
IconButton(
onClick = { imagePicker.launch("image/*") },
enabled = selectedImages.size < 6,
onClick = {
imagePicker.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
enabled = selectedImages.size < 9,
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Outlined.Image,
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)
)
}
@@ -438,7 +441,8 @@ fun CreatePostScreen(
@Composable
private fun SelectedImageItem(
uri: Uri,
onRemove: () -> Unit
onRemove: (() -> Unit)?,
uploadProgress: Float? = null
) {
Box {
AsyncImage(
@@ -449,22 +453,65 @@ private fun SelectedImageItem(
.clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop
)
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
.size(90.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
progress = { uploadProgress },
modifier = Modifier.size(36.dp),
color = 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
)
}
}
}
}

View File

@@ -3,14 +3,18 @@ package com.memory.app.ui.screen
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.RoundedCornerShape
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.MusicNote
import androidx.compose.material.icons.outlined.Image
@@ -42,7 +46,8 @@ fun EditPostScreen(
post: Post,
onClose: () -> Unit,
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) {
@@ -65,9 +70,9 @@ fun EditPostScreen(
val totalImages = existingImages.size + newImages.size
val imagePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = 9)
) { uris ->
val remaining = 6 - totalImages
val remaining = 9 - totalImages
newImages = (newImages + uris).take(remaining.coerceAtLeast(0))
}
@@ -133,12 +138,13 @@ fun EditPostScreen(
}
}
// Content Area
// Content Area - 可滚动
Row(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 20.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState())
) {
Box(
modifier = Modifier
@@ -187,52 +193,35 @@ fun EditPostScreen(
)
)
// Images - 合并已有图片和新图片,超过3张显示为两行
val allImages = existingImages.map { ImageItem.Existing(it) } + newImages.map { ImageItem.New(it) }
// Images - 合并已有图片和新图片,九宫格布局
val allImages = existingImages.map { ImageItem.Existing(it) } + newImages.mapIndexed { idx, uri -> ImageItem.New(uri, idx) }
if (allImages.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
if (allImages.size <= 3) {
// 3张及以下单行显示
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
allImages.forEach { item ->
EditImageItem(
item = item,
onRemove = {
when (item) {
is ImageItem.Existing -> existingImages = existingImages - item.url
is ImageItem.New -> newImages = newImages - item.uri
val columns = if (allImages.size <= 3) allImages.size else 3
val rows = (allImages.size + columns - 1) / columns
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
for (row in 0 until rows) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
for (col in 0 until columns) {
val index = row * columns + col
if (index < allImages.size) {
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)
) {
IconButton(
onClick = { imagePicker.launch("image/*") },
enabled = totalImages < 6,
onClick = {
imagePicker.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
enabled = totalImages < 9,
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Outlined.Image,
contentDescription = "添加图片",
tint = if (totalImages < 6) Brand500 else Slate300,
tint = if (totalImages < 9) Brand500 else Slate300,
modifier = Modifier.size(24.dp)
)
}
@@ -426,13 +419,14 @@ fun EditPostScreen(
// 图片项类型
private sealed class 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
private fun EditImageItem(
item: ImageItem,
onRemove: () -> Unit
onRemove: (() -> Unit)?,
uploadProgress: Float? = null
) {
Box {
AsyncImage(
@@ -446,22 +440,65 @@ private fun EditImageItem(
.clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop
)
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
.size(90.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.Black.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
progress = { uploadProgress },
modifier = Modifier.size(36.dp),
color = 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
)
}
}
}
}

View File

@@ -28,6 +28,7 @@ import com.memory.app.data.model.Comment
import com.memory.app.data.model.Post
import com.memory.app.ui.components.EmojiPickerDialog
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.theme.*
import com.memory.app.util.TimeUtils
@@ -188,21 +189,13 @@ fun PostDetailScreen(
color = MaterialTheme.colorScheme.onSurface
)
// Media
// Media - 使用 MediaGrid 展示缩略图
if (post.media.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
post.media.forEachIndexed { index, media ->
AsyncImage(
model = media.mediaUrl,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable { showImageViewerIndex = index },
contentScale = ContentScale.FillWidth
)
Spacer(modifier = Modifier.height(8.dp))
}
MediaGrid(
media = post.media,
onMediaClick = { index -> showImageViewerIndex = index }
)
}
// Music Card

View File

@@ -10,12 +10,15 @@ import com.memory.app.data.model.CreatePostRequest
import com.memory.app.data.model.Post
import com.memory.app.data.model.UpdatePostRequest
import com.memory.app.data.model.User
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import com.memory.app.data.api.ProgressRequestBody
class HomeViewModel : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
@@ -30,9 +33,16 @@ class HomeViewModel : ViewModel() {
private val _postSuccess = MutableStateFlow(false)
val postSuccess: StateFlow<Boolean> = _postSuccess
private val _editSuccess = MutableStateFlow(false)
val editSuccess: StateFlow<Boolean> = _editSuccess
private val _currentUser = MutableStateFlow<User?>(null)
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 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) {
viewModelScope.launch {
_isPosting.value = true
try {
val response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, mediaIds, visibility, musicUrl))
if (response.isSuccessful) {
// 刷新获取最新数据
_editSuccess.value = true
loadPosts()
}
} catch (e: Exception) {
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) {
viewModelScope.launch {
_isPosting.value = true
_uploadProgress.value = emptyMap()
try {
// 上传新图片
val newImageUrls = mutableListOf<String>()
for (uri in newImages) {
val inputStream = context.contentResolver.openInputStream(uri)
val bytes = inputStream?.readBytes() ?: continue
inputStream.close()
val fileName = "image_${System.currentTimeMillis()}.jpg"
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
val part = MultipartBody.Part.createFormData("file", fileName, requestBody)
val uploadResponse = ApiClient.api.upload(part)
if (uploadResponse.isSuccessful) {
uploadResponse.body()?.url?.let { newImageUrls.add(it) }
// 并发上传新图片(带进度)
val uploadJobs = newImages.mapIndexed { index, uri ->
async {
try {
// 初始化进度
_uploadProgress.value = _uploadProgress.value + (index to 0f)
val inputStream = context.contentResolver.openInputStream(uri)
val bytes = inputStream?.readBytes() ?: return@async null
inputStream.close()
val fileName = "image_${System.currentTimeMillis()}_$index.jpg"
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
// 包装成带进度的 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 response = ApiClient.api.updatePost(postId, UpdatePostRequest(content, allMediaIds, visibility, musicUrl))
if (response.isSuccessful) {
_editSuccess.value = true
loadPosts()
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
_isPosting.value = false
_uploadProgress.value = emptyMap()
}
}
}
@@ -195,24 +233,48 @@ class HomeViewModel : ViewModel() {
viewModelScope.launch {
_isPosting.value = true
_postSuccess.value = false
_uploadProgress.value = emptyMap()
try {
// 上传图片
val imageUrls = mutableListOf<String>()
for (uri in images) {
val inputStream = context.contentResolver.openInputStream(uri)
val bytes = inputStream?.readBytes() ?: continue
inputStream.close()
val fileName = "image_${System.currentTimeMillis()}.jpg"
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
val part = MultipartBody.Part.createFormData("file", fileName, requestBody)
val uploadResponse = ApiClient.api.upload(part)
if (uploadResponse.isSuccessful) {
uploadResponse.body()?.url?.let { imageUrls.add(it) }
// 并发上传图片
val uploadJobs = images.mapIndexed { index, uri ->
async {
try {
// 初始化进度
_uploadProgress.value = _uploadProgress.value + (index to 0f)
val inputStream = context.contentResolver.openInputStream(uri)
val bytes = inputStream?.readBytes() ?: return@async null
inputStream.close()
val fileName = "image_${System.currentTimeMillis()}_$index.jpg"
val requestBody = bytes.toRequestBody("image/*".toMediaTypeOrNull())
// 包装成带进度的 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(
content = content,
@@ -229,6 +291,7 @@ class HomeViewModel : ViewModel() {
e.printStackTrace()
} finally {
_isPosting.value = false
_uploadProgress.value = emptyMap()
}
}
}
@@ -236,4 +299,8 @@ class HomeViewModel : ViewModel() {
fun resetPostSuccess() {
_postSuccess.value = false
}
fun resetEditSuccess() {
_editSuccess.value = false
}
}

View File

@@ -263,38 +263,32 @@ if [ "$BUILD_APK" = true ]; then
echo " 版本: v${new_version} (code: ${new_code})"
echo " 更新日志: ${update_log}"
echo ""
read -p "确认发布 APK? (y/n) " confirm
if [ "$confirm" != "y" ]; then
echo "❌ APK 发布已取消"
BUILD_APK=false
else
# 更新版本号
# 更新版本号
echo "📝 更新版本号..."
update_version_in_gradle $new_version $new_code
# 编译
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 "📝 更新版本号..."
update_version_in_gradle $new_version $new_code
# 编译
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 "🎉 APK 发布完成!"
echo " 版本: v${new_version}"
echo " 下载: ${download_url}"
else
echo ""
echo "❌ 服务器版本更新失败APK 未上传"
exit 1
fi
echo "🎉 APK 发布完成!"
echo " 版本: v${new_version}"
echo " 下载: ${download_url}"
else
echo ""
echo "❌ 服务器版本更新失败APK 未上传"
exit 1
fi
fi