feat:编辑或者新增推文时自动解析输入的音乐链接
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.memory.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 30
|
||||
versionName = "1.4.6"
|
||||
versionCode = 31
|
||||
versionName = "1.4.7"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "30")
|
||||
buildConfigField("int", "VERSION_CODE", "31")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -104,6 +104,10 @@ interface ApiService {
|
||||
@POST("upload")
|
||||
suspend fun upload(@Part file: MultipartBody.Part): Response<UploadResponse>
|
||||
|
||||
// Music
|
||||
@POST("music/parse")
|
||||
suspend fun parseMusic(@Body request: ParseMusicRequest): Response<ParseMusicResponse>
|
||||
|
||||
// Admin
|
||||
@GET("admin/settings")
|
||||
suspend fun getSettings(): Response<Map<String, String>>
|
||||
|
||||
@@ -223,3 +223,17 @@ data class Notification(
|
||||
data class UnreadCountResponse(
|
||||
val count: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ParseMusicRequest(
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ParseMusicResponse(
|
||||
val title: String,
|
||||
val artist: String,
|
||||
val cover: String = "",
|
||||
@SerialName("share_url") val shareUrl: String,
|
||||
val platform: String = "qqmusic"
|
||||
)
|
||||
|
||||
@@ -29,8 +29,13 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.memory.app.data.api.ApiClient
|
||||
import com.memory.app.data.model.MusicInfo
|
||||
import com.memory.app.data.model.ParseMusicRequest
|
||||
import com.memory.app.data.model.User
|
||||
import com.memory.app.ui.components.MusicCard
|
||||
import com.memory.app.ui.theme.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CreatePostScreen(
|
||||
@@ -49,7 +54,10 @@ fun CreatePostScreen(
|
||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||
var visibility by remember { mutableStateOf(0) } // 0=所有人可见, 1=仅自己可见
|
||||
var musicUrl by remember { mutableStateOf("") }
|
||||
var parsedMusic by remember { mutableStateOf<MusicInfo?>(null) }
|
||||
var isMusicLoading by remember { mutableStateOf(false) }
|
||||
var showMusicInput by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val imagePicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetMultipleContents()
|
||||
@@ -216,8 +224,37 @@ fun CreatePostScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Music Link Display
|
||||
if (musicUrl.isNotBlank()) {
|
||||
// Music Display
|
||||
if (parsedMusic != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Box {
|
||||
MusicCard(
|
||||
music = parsedMusic!!,
|
||||
onClick = { }
|
||||
)
|
||||
// 删除按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.clickable {
|
||||
musicUrl = ""
|
||||
parsedMusic = null
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "移除音乐",
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (isMusicLoading) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
@@ -225,33 +262,21 @@ fun CreatePostScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = Brand500,
|
||||
modifier = Modifier.size(20.dp)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Brand500,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "已添加音乐链接",
|
||||
text = "正在解析音乐链接...",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
IconButton(
|
||||
onClick = { musicUrl = "" },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "移除",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,13 +314,13 @@ fun CreatePostScreen(
|
||||
// Music Button
|
||||
IconButton(
|
||||
onClick = { showMusicInput = true },
|
||||
enabled = musicUrl.isBlank(),
|
||||
enabled = parsedMusic == null && !isMusicLoading,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MusicNote,
|
||||
contentDescription = "添加音乐",
|
||||
tint = if (musicUrl.isBlank()) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
tint = if (parsedMusic == null && !isMusicLoading) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -376,8 +401,35 @@ fun CreatePostScreen(
|
||||
MusicLinkInputDialog(
|
||||
onDismiss = { showMusicInput = false },
|
||||
onConfirm = { url ->
|
||||
musicUrl = url
|
||||
showMusicInput = false
|
||||
musicUrl = url
|
||||
isMusicLoading = true
|
||||
scope.launch {
|
||||
try {
|
||||
val response = ApiClient.api.parseMusic(ParseMusicRequest(url))
|
||||
if (response.isSuccessful) {
|
||||
response.body()?.let { result ->
|
||||
parsedMusic = MusicInfo(
|
||||
id = 0,
|
||||
postId = 0,
|
||||
title = result.title,
|
||||
artist = result.artist,
|
||||
cover = result.cover,
|
||||
shareUrl = result.shareUrl,
|
||||
platform = result.platform
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 解析失败,清空
|
||||
musicUrl = ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
musicUrl = ""
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
isMusicLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.memory.app.data.api.ApiClient
|
||||
import com.memory.app.data.model.MusicInfo
|
||||
import com.memory.app.data.model.ParseMusicRequest
|
||||
import com.memory.app.data.model.Post
|
||||
import com.memory.app.ui.components.MusicCard
|
||||
import com.memory.app.ui.theme.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EditPostScreen(
|
||||
@@ -51,7 +55,10 @@ fun EditPostScreen(
|
||||
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||
var visibility by remember { mutableStateOf(post.visibility) }
|
||||
var musicUrl by remember { mutableStateOf(post.music?.shareUrl ?: "") }
|
||||
var parsedMusic by remember { mutableStateOf(post.music) }
|
||||
var isMusicLoading by remember { mutableStateOf(false) }
|
||||
var showMusicInput by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
// 标记是否有原始音乐(用于判断是删除还是新增)
|
||||
val hadOriginalMusic = post.music != null
|
||||
|
||||
@@ -232,71 +239,59 @@ fun EditPostScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Music Card - 显示已有音乐或新添加的音乐链接
|
||||
if (musicUrl.isNotBlank()) {
|
||||
// Music Display
|
||||
if (parsedMusic != null) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (post.music != null && musicUrl == post.music.shareUrl) {
|
||||
// 显示已有音乐卡片(带删除按钮)
|
||||
Box {
|
||||
MusicCard(
|
||||
music = post.music,
|
||||
onClick = { }
|
||||
)
|
||||
// 删除按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.clickable { musicUrl = "" },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "移除音乐",
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 显示新添加的音乐链接
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Box {
|
||||
MusicCard(
|
||||
music = parsedMusic!!,
|
||||
onClick = { }
|
||||
)
|
||||
// 删除按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.clickable {
|
||||
musicUrl = ""
|
||||
parsedMusic = null
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = Brand500,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "已添加音乐链接",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(
|
||||
onClick = { musicUrl = "" },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "移除",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "移除音乐",
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (isMusicLoading) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Brand500,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "正在解析音乐链接...",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,13 +324,13 @@ fun EditPostScreen(
|
||||
// Music Button
|
||||
IconButton(
|
||||
onClick = { showMusicInput = true },
|
||||
enabled = musicUrl.isBlank(),
|
||||
enabled = parsedMusic == null && !isMusicLoading,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MusicNote,
|
||||
contentDescription = "添加音乐",
|
||||
tint = if (musicUrl.isBlank()) Brand500 else Slate300,
|
||||
tint = if (parsedMusic == null && !isMusicLoading) Brand500 else Slate300,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
@@ -395,8 +390,34 @@ fun EditPostScreen(
|
||||
EditMusicLinkInputDialog(
|
||||
onDismiss = { showMusicInput = false },
|
||||
onConfirm = { url ->
|
||||
musicUrl = url
|
||||
showMusicInput = false
|
||||
musicUrl = url
|
||||
isMusicLoading = true
|
||||
scope.launch {
|
||||
try {
|
||||
val response = ApiClient.api.parseMusic(ParseMusicRequest(url))
|
||||
if (response.isSuccessful) {
|
||||
response.body()?.let { result ->
|
||||
parsedMusic = MusicInfo(
|
||||
id = 0,
|
||||
postId = 0,
|
||||
title = result.title,
|
||||
artist = result.artist,
|
||||
cover = result.cover,
|
||||
shareUrl = result.shareUrl,
|
||||
platform = result.platform
|
||||
)
|
||||
}
|
||||
} else {
|
||||
musicUrl = ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
musicUrl = ""
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
isMusicLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -272,9 +272,9 @@ fun PostDetailScreen(
|
||||
.padding(vertical = 48.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 沙发 + 落地灯图标
|
||||
// 沙发图标
|
||||
Text(
|
||||
text = "🛋️🪔",
|
||||
text = "🛋️",
|
||||
fontSize = 48.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// QQ音乐链接解析
|
||||
@@ -174,3 +176,35 @@ func IsQQMusicURL(url string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// ParseMusicURL 解析音乐链接 API
|
||||
func ParseMusicURL(c *gin.Context) {
|
||||
var req struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if !IsQQMusicURL(req.URL) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的音乐链接"})
|
||||
return
|
||||
}
|
||||
|
||||
parser := &QQMusicParser{}
|
||||
result, err := parser.Parse(req.URL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "解析失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"title": result.Title,
|
||||
"artist": result.Artist,
|
||||
"cover": result.Cover,
|
||||
"share_url": result.ShareURL,
|
||||
"platform": result.Platform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@ func Setup(db *sql.DB, cfg *config.Config) *gin.Engine {
|
||||
// 上传
|
||||
auth.POST("/upload", uploadHandler.Upload)
|
||||
|
||||
// 音乐解析
|
||||
auth.POST("/music/parse", handler.ParseMusicURL)
|
||||
|
||||
// 通知
|
||||
auth.GET("/notifications", notificationHandler.List)
|
||||
auth.GET("/notifications/unread", notificationHandler.UnreadCount)
|
||||
|
||||
Reference in New Issue
Block a user