feat:编辑或者新增推文时自动解析输入的音乐链接

This commit is contained in:
amos
2025-12-30 14:30:40 +08:00
parent c9389e4caa
commit 91abbff2b1
8 changed files with 226 additions and 98 deletions

View File

@@ -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 {

View File

@@ -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>>

View File

@@ -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"
)

View File

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

View File

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

View File

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

View File

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

View File

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