feat(android): add memoir chapter image models and content block parsing

Made-with: Cursor
This commit is contained in:
Kevin
2026-03-10 16:07:46 +08:00
parent f5afeb39ef
commit 2cecbd84ce
4 changed files with 115 additions and 4 deletions

View File

@@ -18,6 +18,23 @@ data class BookDto(
val last_update_chapter_id: String? = null
)
// 章节图片元数据DTO
@Serializable
data class ChapterImageDto(
val index: Int,
val placeholder: String,
val description: String,
val prompt: String? = null,
val url: String? = null,
val status: String,
val provider: String? = null,
val style: String? = null,
val size: String? = null,
val error: String? = null,
val created_at: String? = null,
val updated_at: String? = null,
)
// 章节信息DTO匹配服务器格式
@Serializable
data class ChapterDto(
@@ -27,7 +44,7 @@ data class ChapterDto(
val order_index: Int,
val status: String, // "draft", "partial", "completed"
val category: String,
val images: List<String> = emptyList(),
val images: List<ChapterImageDto> = emptyList(),
val updated_at: String? = null,
val is_new: Boolean = false,
val source_segments: List<String> = emptyList()
@@ -44,8 +61,8 @@ data class ChapterContentDto(
val category: String,
val pageCount: Int?,
val updatedAt: Long,
val quotes: List<String> = emptyList(), // 引用内容列表
val images: List<String> = emptyList() // 图片列表
val quotes: List<String> = emptyList(),
val images: List<ChapterImageDto> = emptyList()
)
// 回忆录状态DTO

View File

@@ -173,7 +173,7 @@ private fun FilledChapterCard(
) {
val processedContent = TextUtils.removeImagePlaceholders(
chapter.content,
hasImages = chapter.images.isNotEmpty()
hasImages = chapter.images.any { it.status == "completed" && !it.url.isNullOrBlank() }
)
MarkdownText(

View File

@@ -0,0 +1,35 @@
package com.huaga.life_echo.ui.components.memoir
import com.huaga.life_echo.network.models.ChapterImageDto
sealed interface MemoirContentBlock {
data class Text(val content: String) : MemoirContentBlock
data class Image(val image: ChapterImageDto) : MemoirContentBlock
}
private val imagePlaceholderRegex =
Regex("""\{\{\{\{IMAGE:.*?\}\}\}\}|\{\{IMAGE:.*?\}\}""", setOf(RegexOption.DOT_MATCHES_ALL))
private fun stripImagePlaceholders(text: String): String =
text.replace(imagePlaceholderRegex, "").trim()
fun splitMemoirContent(content: String, images: List<ChapterImageDto>): List<MemoirContentBlock> {
var remaining = content
val blocks = mutableListOf<MemoirContentBlock>()
images.sortedBy { it.index }.forEach { image ->
val placeholder = image.placeholder
if (!remaining.contains(placeholder)) return@forEach
val parts = remaining.split(placeholder, limit = 2)
val before = stripImagePlaceholders(parts.first())
if (before.isNotBlank()) blocks += MemoirContentBlock.Text(before)
if (image.status == "completed" && !image.url.isNullOrBlank()) {
blocks += MemoirContentBlock.Image(image)
} else if (image.status in listOf("pending", "processing", "failed")) {
blocks += MemoirContentBlock.Image(image)
}
remaining = parts.getOrElse(1) { "" }
}
val trailingText = stripImagePlaceholders(remaining)
if (trailingText.isNotBlank()) blocks += MemoirContentBlock.Text(trailingText)
return blocks
}

View File

@@ -0,0 +1,59 @@
package com.huaga.life_echo.ui.components.memoir
import com.huaga.life_echo.network.models.ChapterImageDto
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class MemoirContentBlocksTest {
@Test
fun splitMemoirContent_insertsImageBlockForCompletedImage_andDropsFailedPlaceholder() {
val blocks = splitMemoirContent(
content = "那条路我一直记得。\n\n{{{{IMAGE:南方小镇的青石板路}}}}\n\n后来就下雨了。",
images = listOf(
ChapterImageDto(
index = 0,
placeholder = "{{{{IMAGE:南方小镇的青石板路}}}}",
description = "南方小镇的青石板路",
prompt = null,
url = "https://cos.example.com/0.png",
status = "completed",
provider = "liblib",
style = "watercolor",
size = "1024x1024",
error = null,
created_at = null,
updated_at = null,
)
)
)
assertEquals(MemoirContentBlock.Text::class, blocks[0]::class)
assertEquals(MemoirContentBlock.Image::class, blocks[1]::class)
assertTrue((blocks[1] as MemoirContentBlock.Image).image.url!!.contains("cos.example.com"))
}
@Test
fun splitMemoirContent_returnsOnlyTextBlock_whenNoImages() {
val blocks = splitMemoirContent(
content = "那条路我一直记得。\n\n后来就下雨了。",
images = emptyList()
)
assertEquals(1, blocks.size)
assertEquals(MemoirContentBlock.Text::class, blocks[0]::class)
}
@Test
fun splitMemoirContent_stripsOrphanPlaceholders() {
val blocks = splitMemoirContent(
content = "开头。\n\n{{{{IMAGE:被忽略的图}}}}\n\n结尾。",
images = emptyList()
)
assertEquals(1, blocks.size)
val text = (blocks[0] as MemoirContentBlock.Text).content
assertTrue(!text.contains("IMAGE:"))
}
}