diff --git a/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt b/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt index cea9db2..34f86d9 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/network/models/MemoirModels.kt @@ -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 = emptyList(), + val images: List = emptyList(), val updated_at: String? = null, val is_new: Boolean = false, val source_segments: List = emptyList() @@ -44,8 +61,8 @@ data class ChapterContentDto( val category: String, val pageCount: Int?, val updatedAt: Long, - val quotes: List = emptyList(), // 引用内容列表 - val images: List = emptyList() // 图片列表 + val quotes: List = emptyList(), + val images: List = emptyList() ) // 回忆录状态DTO diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterCard.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterCard.kt index 9de83ec..42b3cf2 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterCard.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/ChapterCard.kt @@ -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( diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocks.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocks.kt new file mode 100644 index 0000000..d36265a --- /dev/null +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocks.kt @@ -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): List { + var remaining = content + val blocks = mutableListOf() + 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 +} diff --git a/app-android/app/src/test/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocksTest.kt b/app-android/app/src/test/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocksTest.kt new file mode 100644 index 0000000..d840d24 --- /dev/null +++ b/app-android/app/src/test/java/com/huaga/life_echo/ui/components/memoir/MemoirContentBlocksTest.kt @@ -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:")) + } +}