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
|
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(匹配服务器格式)
|
// 章节信息DTO(匹配服务器格式)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterDto(
|
data class ChapterDto(
|
||||||
@@ -27,7 +44,7 @@ data class ChapterDto(
|
|||||||
val order_index: Int,
|
val order_index: Int,
|
||||||
val status: String, // "draft", "partial", "completed"
|
val status: String, // "draft", "partial", "completed"
|
||||||
val category: String,
|
val category: String,
|
||||||
val images: List<String> = emptyList(),
|
val images: List<ChapterImageDto> = emptyList(),
|
||||||
val updated_at: String? = null,
|
val updated_at: String? = null,
|
||||||
val is_new: Boolean = false,
|
val is_new: Boolean = false,
|
||||||
val source_segments: List<String> = emptyList()
|
val source_segments: List<String> = emptyList()
|
||||||
@@ -44,8 +61,8 @@ data class ChapterContentDto(
|
|||||||
val category: String,
|
val category: String,
|
||||||
val pageCount: Int?,
|
val pageCount: Int?,
|
||||||
val updatedAt: Long,
|
val updatedAt: Long,
|
||||||
val quotes: List<String> = emptyList(), // 引用内容列表
|
val quotes: List<String> = emptyList(),
|
||||||
val images: List<String> = emptyList() // 图片列表
|
val images: List<ChapterImageDto> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
// 回忆录状态DTO
|
// 回忆录状态DTO
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ private fun FilledChapterCard(
|
|||||||
) {
|
) {
|
||||||
val processedContent = TextUtils.removeImagePlaceholders(
|
val processedContent = TextUtils.removeImagePlaceholders(
|
||||||
chapter.content,
|
chapter.content,
|
||||||
hasImages = chapter.images.isNotEmpty()
|
hasImages = chapter.images.any { it.status == "completed" && !it.url.isNullOrBlank() }
|
||||||
)
|
)
|
||||||
|
|
||||||
MarkdownText(
|
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