feat(android): add memoir chapter image models and content block parsing
Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user