support for poetry and epigraphs

This commit is contained in:
sergeych committed 2026-05-24 09:31:32 +03:00
1 parent 316b10d015
commit de04b9cbed
11 files changed
+787 -10

No files matched your search

@@ -377,9 +377,15 @@ actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
val clusterId = file.bodyClusterId ?: return@useLibrary BookInfoExtras()
val sourceFileName = file.originalFilename ?: file.storageUri?.substringAfterLast('/')
val clusterId = file.bodyClusterId ?: return@useLibrary BookInfoExtras(
sourceFileName = sourceFileName,
sourceFilePath = file.storageUri,
)
val readingPosition = db.readingStates.getForBodyCluster(clusterId)?.anchor?.formatHintsJson?.toReadingPosition()
BookInfoExtras(
sourceFileName = sourceFileName,
sourceFilePath = file.storageUri,
bookmarks = db.bookmarks.listForBodyCluster(clusterId).map {
BookmarkInfo(
title = it.title,
@@ -92,6 +92,16 @@ internal fun BookInfoScreen(
}
}
}
item {
InfoSection(strings.sourceFile) {
if (extras == null) {
Text(strings.loading, style = MaterialTheme.typography.bodyMedium)
} else {
DetailLine(strings.fileName, extras?.sourceFileName?.ifBlank { null } ?: strings.notSpecified)
DetailLine(strings.filePath, extras?.sourceFilePath?.ifBlank { null } ?: strings.notSpecified)
}
}
}
item {
InfoSection(strings.lastReadingPosition) {
val position = extras?.lastReadingPosition
@@ -64,6 +64,8 @@ data class ReadingPosition(
)
data class BookInfoExtras(
val sourceFileName: String? = null,
val sourceFilePath: String? = null,
val bookmarks: List<BookmarkInfo> = emptyList(),
val notes: List<NoteInfo> = emptyList(),
val lastReadingPosition: ReadingPosition? = null,
@@ -117,6 +117,9 @@ internal open class AppStrings {
open val words = "Words"
open val sections = "Sections"
open val images = "Images"
open val sourceFile = "Source file"
open val fileName = "File name"
open val filePath = "Path"
open val lastReadingPosition = "Last Reading Position"
open val noSavedPosition = "No saved position"
open val listItem = "List item"
@@ -243,7 +246,7 @@ internal object RussianStrings : AppStrings() {
override val couldNotOpenBook = "Не удалось открыть книгу."
override val couldNotUpdateBook = "Не удалось обновить книгу."
override val bookFileNotAvailable = "Файл книги недоступен."
override val scanFailed = "Сканирование не удалось."
override val scanFailed = "Импорт не удался."
override val searchFailed = "Поиск не удался."
override val libraryRescanFailed = "Повторное сканирование библиотеки не удалось."
override val rescanningLibrary = "Пересканируем библиотеку..."
@@ -287,7 +290,7 @@ internal object RussianStrings : AppStrings() {
override val filterToRead = "К чтению"
override val filterRead = "Прочитанные"
override val scan = "Сканирование"
override val scan = "Импорт"
override val rootFolder = "Корневая папка"
override val choose = "Выбрать"
override val logPrefix = "Лог"
@@ -309,6 +312,9 @@ internal object RussianStrings : AppStrings() {
override val words = "Слова"
override val sections = "Разделы"
override val images = "Иллюстрации"
override val sourceFile = "Исходный файл"
override val fileName = "Имя файла"
override val filePath = "Путь"
override val lastReadingPosition = "Последняя позиция чтения"
override val noSavedPosition = "Позиция не сохранена"
override val listItem = "Элемент списка"
@@ -79,7 +79,12 @@ import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp
import net.sergeych.toread.fb2.Fb2Block
import net.sergeych.toread.fb2.Fb2Book
import net.sergeych.toread.fb2.Fb2Cite
import net.sergeych.toread.fb2.Fb2Epigraph
import net.sergeych.toread.fb2.Fb2EpigraphBlock
import net.sergeych.toread.fb2.Fb2ImageRef
import net.sergeych.toread.fb2.Fb2Poem
import net.sergeych.toread.fb2.Fb2PoemBlock
import net.sergeych.toread.fb2.Fb2Section
import net.sergeych.toread.fb2.Fb2Text
import net.sergeych.toread.fb2.Fb2TextSpan
@@ -210,6 +215,27 @@ internal fun ContinuousBookReader(
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
onTextLayout = { textLineMetricsByItem[itemIndex] = it.toTextLineMetrics() },
)
is ReaderElement.Epigraph -> ReaderEpigraph(
epigraph = element.epigraph,
language = book.language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = element.depth,
)
is ReaderElement.Cite -> ReaderCite(
cite = element.cite,
language = book.language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = element.depth,
)
is ReaderElement.Poem -> ReaderPoem(
poem = element.poem,
language = book.language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = element.depth,
)
}
}
}
@@ -467,6 +493,13 @@ private fun ReaderPane(book: Fb2Book, section: Fb2Section?, modifier: Modifier =
style = readerParagraphTextStyle(book.language),
textAlign = TextAlign.Unspecified,
)
is Fb2Block.Poem -> ReaderPoem(
poem = block.poem,
language = book.language,
hyphenation = hyphenation,
highlightedRange = null,
depth = 0,
)
is Fb2Block.Subtitle -> ReaderText(
text = block.content,
language = book.language,
@@ -475,6 +508,20 @@ private fun ReaderPane(book: Fb2Book, section: Fb2Section?, modifier: Modifier =
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
)
is Fb2Block.Epigraph -> ReaderEpigraph(
epigraph = block.epigraph,
language = book.language,
hyphenation = hyphenation,
highlightedRange = null,
depth = 0,
)
is Fb2Block.Cite -> ReaderCite(
cite = block.cite,
language = book.language,
hyphenation = hyphenation,
highlightedRange = null,
depth = 0,
)
}
}
item { Spacer(Modifier.height(22.dp)) }
@@ -543,6 +590,212 @@ private fun ReaderText(
)
}
@Composable
private fun ReaderPoem(
poem: Fb2Poem,
language: String?,
hyphenation: HyphenationRegistry,
highlightedRange: ReaderSentenceRange?,
depth: Int,
modifier: Modifier = Modifier,
) {
val segments = remember(poem) { poem.readerSegments() }
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = (depth * 8).dp, top = 8.dp, bottom = 8.dp),
) {
poem.epigraphs.forEachIndexed { index, epigraph ->
ReaderEpigraph(
epigraph = epigraph,
language = language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = 0,
modifier = Modifier.padding(top = if (index == 0) 0.dp else 8.dp, bottom = 8.dp),
)
}
segments.forEach { segment ->
if (segment.gapBeforeDp > 0) {
Spacer(Modifier.height(segment.gapBeforeDp.dp))
}
ReaderText(
text = segment.text,
language = language,
hyphenation = hyphenation,
style = poemTextStyle(segment.kind, language),
highlightedRange = highlightedRange?.forSegment(segment),
textAlign = when (segment.kind) {
ReaderPoemSegmentKind.Title,
ReaderPoemSegmentKind.Subtitle -> TextAlign.Center
ReaderPoemSegmentKind.TextAuthor,
ReaderPoemSegmentKind.Date -> TextAlign.End
ReaderPoemSegmentKind.Verse -> TextAlign.Start
},
modifier = when (segment.kind) {
ReaderPoemSegmentKind.Verse -> Modifier.padding(start = 22.dp)
else -> Modifier
},
)
}
}
}
@Composable
private fun ReaderEpigraph(
epigraph: Fb2Epigraph,
language: String?,
hyphenation: HyphenationRegistry,
highlightedRange: ReaderSentenceRange?,
depth: Int,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = (depth * 8 + 26).dp, end = 18.dp, top = 8.dp, bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
epigraph.blocks.forEach { block ->
ReaderEpigraphBlock(
block = block,
language = language,
hyphenation = hyphenation,
highlightedRange = null,
depth = 0,
)
}
epigraph.textAuthors.forEach { author ->
ReaderText(
text = author,
language = language,
hyphenation = hyphenation,
style = epigraphAuthorTextStyle(language),
highlightedRange = null,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
)
}
}
}
@Composable
private fun ReaderCite(
cite: Fb2Cite,
language: String?,
hyphenation: HyphenationRegistry,
highlightedRange: ReaderSentenceRange?,
depth: Int,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(start = (depth * 8 + 22).dp, end = 14.dp, top = 8.dp, bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
cite.blocks.forEach { block ->
ReaderEpigraphBlock(
block = block,
language = language,
hyphenation = hyphenation,
highlightedRange = null,
depth = 0,
)
}
cite.textAuthors.forEach { author ->
ReaderText(
text = author,
language = language,
hyphenation = hyphenation,
style = epigraphAuthorTextStyle(language),
highlightedRange = null,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
)
}
}
}
@Composable
private fun ReaderEpigraphBlock(
block: Fb2EpigraphBlock,
language: String?,
hyphenation: HyphenationRegistry,
highlightedRange: ReaderSentenceRange?,
depth: Int,
) {
when (block) {
Fb2EpigraphBlock.EmptyLine -> Spacer(Modifier.height(12.dp))
is Fb2EpigraphBlock.Paragraph -> ReaderText(
text = block.content,
language = language,
hyphenation = hyphenation,
style = epigraphTextStyle(language),
highlightedRange = highlightedRange,
textAlign = TextAlign.Start,
)
is Fb2EpigraphBlock.Subtitle -> ReaderText(
text = block.content,
language = language,
hyphenation = hyphenation,
style = epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold),
highlightedRange = highlightedRange,
textAlign = TextAlign.Center,
)
is Fb2EpigraphBlock.Poem -> ReaderPoem(
poem = block.poem,
language = language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = depth,
)
is Fb2EpigraphBlock.Cite -> ReaderCite(
cite = block.cite,
language = language,
hyphenation = hyphenation,
highlightedRange = highlightedRange,
depth = depth,
)
}
}
@Composable
private fun poemTextStyle(kind: ReaderPoemSegmentKind, language: String?): TextStyle =
when (kind) {
ReaderPoemSegmentKind.Title -> MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.SemiBold,
lineHeight = 26.sp,
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
)
ReaderPoemSegmentKind.Subtitle -> MaterialTheme.typography.bodyLarge.copy(
fontStyle = FontStyle.Italic,
lineHeight = 24.sp,
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
)
ReaderPoemSegmentKind.Verse -> readerParagraphTextStyle(language).copy(
lineHeight = 24.sp,
)
ReaderPoemSegmentKind.TextAuthor,
ReaderPoemSegmentKind.Date -> MaterialTheme.typography.bodyMedium.copy(
fontStyle = FontStyle.Italic,
lineHeight = 22.sp,
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
)
}
@Composable
private fun epigraphTextStyle(language: String?): TextStyle =
MaterialTheme.typography.bodyMedium.copy(
fontStyle = FontStyle.Italic,
lineHeight = 22.sp,
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
)
@Composable
private fun epigraphAuthorTextStyle(language: String?): TextStyle =
epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold)
@Composable
private fun readerParagraphTextStyle(language: String?): TextStyle =
MaterialTheme.typography.bodyLarge.copy(
@@ -766,6 +1019,7 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
fun addTextSentences(
itemIndex: Int,
text: Fb2Text,
offset: Int = 0,
pauseBeforeMillis: Long = 0,
pauseAfterMillis: Long = 0,
) {
@@ -784,8 +1038,8 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
sentences += ReadAloudSentence(
index = sentences.size,
itemIndex = itemIndex,
start = range.start,
endExclusive = range.endExclusive,
start = offset + range.start,
endExclusive = offset + range.endExclusive,
text = sentenceText,
spokenText = spokenText,
pauseBeforeMillis = effectivePauseBefore,
@@ -795,6 +1049,43 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
}
}
lateinit var addPoemSentences: (Int, Fb2Poem) -> Unit
lateinit var addCiteSentences: (Int, Fb2Cite) -> Unit
lateinit var addEpigraphSentences: (Int, Fb2Epigraph) -> Unit
addPoemSentences = { itemIndex, poem ->
poem.epigraphs.forEach { epigraph -> addEpigraphSentences(itemIndex, epigraph) }
poem.readerSegments().forEach { segment ->
addTextSentences(itemIndex, segment.text, offset = segment.startOffset)
}
}
addCiteSentences = { itemIndex, cite ->
cite.blocks.forEach { block ->
when (block) {
Fb2EpigraphBlock.EmptyLine -> Unit
is Fb2EpigraphBlock.Paragraph -> addTextSentences(itemIndex, block.content)
is Fb2EpigraphBlock.Subtitle -> addTextSentences(itemIndex, block.content)
is Fb2EpigraphBlock.Poem -> addPoemSentences(itemIndex, block.poem)
is Fb2EpigraphBlock.Cite -> addCiteSentences(itemIndex, block.cite)
}
}
cite.textAuthors.forEach { addTextSentences(itemIndex, it) }
}
addEpigraphSentences = { itemIndex, epigraph ->
epigraph.blocks.forEach { block ->
when (block) {
Fb2EpigraphBlock.EmptyLine -> Unit
is Fb2EpigraphBlock.Paragraph -> addTextSentences(itemIndex, block.content)
is Fb2EpigraphBlock.Subtitle -> addTextSentences(itemIndex, block.content)
is Fb2EpigraphBlock.Poem -> addPoemSentences(itemIndex, block.poem)
is Fb2EpigraphBlock.Cite -> addCiteSentences(itemIndex, block.cite)
}
}
epigraph.textAuthors.forEach { addTextSentences(itemIndex, it) }
}
fun addSection(section: Fb2Section, depth: Int) {
if (section.title.isNullOrBlank()) {
elements += ReaderElement.SectionSeparator
@@ -821,6 +1112,10 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
addTextSentences(itemIndex, block.content)
elements += ReaderElement.Paragraph(block.content, depth)
}
is Fb2Block.Poem -> {
addPoemSentences(itemIndex, block.poem)
elements += ReaderElement.Poem(block.poem, depth)
}
is Fb2Block.Subtitle -> {
addTextSentences(
itemIndex = itemIndex,
@@ -830,6 +1125,14 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
)
elements += ReaderElement.Subtitle(block.content)
}
is Fb2Block.Epigraph -> {
addEpigraphSentences(itemIndex, block.epigraph)
elements += ReaderElement.Epigraph(block.epigraph, depth)
}
is Fb2Block.Cite -> {
addCiteSentences(itemIndex, block.cite)
elements += ReaderElement.Cite(block.cite, depth)
}
}
}
section.sections.forEach { addSection(it, depth + 1) }
@@ -837,6 +1140,11 @@ internal fun buildReaderContentPlan(book: Fb2Book): ReaderContentPlan {
elements += ReaderElement.Cover
elements += ReaderElement.FixedSpacer(6)
book.bodyEpigraphs.forEach { epigraph ->
val itemIndex = elements.size
addEpigraphSentences(itemIndex, epigraph)
elements += ReaderElement.Epigraph(epigraph, 0)
}
book.sections.forEach { addSection(it, 0) }
elements += ReaderElement.FixedSpacer(22)
@@ -866,6 +1174,80 @@ internal sealed interface ReaderElement {
data class BookImage(val image: Fb2ImageRef) : ReaderElement
data class Paragraph(val text: Fb2Text, val depth: Int) : ReaderElement
data class Subtitle(val text: Fb2Text) : ReaderElement
data class Epigraph(val epigraph: Fb2Epigraph, val depth: Int) : ReaderElement
data class Cite(val cite: Fb2Cite, val depth: Int) : ReaderElement
data class Poem(val poem: Fb2Poem, val depth: Int) : ReaderElement
}
internal data class ReaderPoemSegment(
val text: Fb2Text,
val kind: ReaderPoemSegmentKind,
val startOffset: Int,
val gapBeforeDp: Int,
)
internal enum class ReaderPoemSegmentKind {
Title,
Subtitle,
Verse,
TextAuthor,
Date,
}
private fun Fb2Poem.readerSegments(): List<ReaderPoemSegment> {
val segments = mutableListOf<ReaderPoemSegment>()
var offset = 0
fun add(text: Fb2Text, kind: ReaderPoemSegmentKind, gapBeforeDp: Int) {
segments += ReaderPoemSegment(text, kind, offset, gapBeforeDp)
offset += text.plainText().length + 1
}
title.forEachIndexed { index, text ->
add(text, ReaderPoemSegmentKind.Title, if (index == 0) 0 else 2)
}
blocks.forEach { block ->
when (block) {
is Fb2PoemBlock.Subtitle -> add(block.content, ReaderPoemSegmentKind.Subtitle, 8)
is Fb2PoemBlock.Stanza -> {
val stanza = block.stanza
val startsNewStanza = segments.isNotEmpty()
stanza.title.forEachIndexed { index, text ->
add(text, ReaderPoemSegmentKind.Subtitle, if (startsNewStanza && index == 0) 12 else 4)
}
stanza.subtitle?.let { add(it, ReaderPoemSegmentKind.Subtitle, 4) }
stanza.verses.forEachIndexed { index, verse ->
val gap = when {
index == 0 && startsNewStanza && stanza.title.isEmpty() && stanza.subtitle == null -> 12
index == 0 -> 4
else -> 0
}
add(verse, ReaderPoemSegmentKind.Verse, gap)
}
}
}
}
textAuthors.forEachIndexed { index, text ->
add(text, ReaderPoemSegmentKind.TextAuthor, if (index == 0) 8 else 2)
}
date?.takeIf { it.isNotBlank() }?.let { date ->
add(Fb2Text(listOf(Fb2TextSpan(date))), ReaderPoemSegmentKind.Date, 2)
}
return segments
}
private fun ReaderSentenceRange.forSegment(segment: ReaderPoemSegment): ReaderSentenceRange? {
val segmentStart = segment.startOffset
val segmentEnd = segmentStart + segment.text.plainText().length
val overlapStart = max(start, segmentStart)
val overlapEnd = min(endExclusive, segmentEnd)
if (overlapStart >= overlapEnd) return null
return ReaderSentenceRange(
start = overlapStart - segmentStart,
endExclusive = overlapEnd - segmentStart,
pauseAfterMillis = pauseAfterMillis,
)
}
private data class ReaderSentenceRange(
@@ -1006,10 +1388,42 @@ internal data class BookStats(
fun from(book: Fb2Book): BookStats {
val sections = book.sections.flattenSections()
val words = sections.sumOf { entry ->
entry.section.paragraphs.sumOf { paragraph -> paragraph.split(Regex("\\s+")).count { it.isNotBlank() } }
entry.section.readableBlocks().sumOf { block -> block.wordCount() }
}
val bodyImages = sections.sumOf { it.section.images.size } + book.bodyImages.size
return BookStats(words = words, sections = sections.size, images = bodyImages + book.coverImages.size)
}
}
}
private fun Fb2Block.wordCount(): Int =
when (this) {
Fb2Block.EmptyLine,
is Fb2Block.Image -> 0
is Fb2Block.Cite -> cite.wordCount()
is Fb2Block.Epigraph -> epigraph.wordCount()
is Fb2Block.Paragraph -> content.plainText().wordCount()
is Fb2Block.Poem -> poem.wordCount()
is Fb2Block.Subtitle -> content.plainText().wordCount()
}
private fun Fb2Poem.wordCount(): Int =
epigraphs.sumOf { it.wordCount() } + readerSegments().sumOf { it.text.plainText().wordCount() }
private fun Fb2Epigraph.wordCount(): Int =
blocks.sumOf { it.wordCount() } + textAuthors.sumOf { it.plainText().wordCount() }
private fun Fb2Cite.wordCount(): Int =
blocks.sumOf { it.wordCount() } + textAuthors.sumOf { it.plainText().wordCount() }
private fun Fb2EpigraphBlock.wordCount(): Int =
when (this) {
Fb2EpigraphBlock.EmptyLine -> 0
is Fb2EpigraphBlock.Cite -> cite.wordCount()
is Fb2EpigraphBlock.Paragraph -> content.plainText().wordCount()
is Fb2EpigraphBlock.Poem -> poem.wordCount()
is Fb2EpigraphBlock.Subtitle -> content.plainText().wordCount()
}
private fun String.wordCount(): Int =
split(Regex("\\s+")).count { it.isNotBlank() }
@@ -4,7 +4,12 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import net.sergeych.toread.fb2.Fb2Block
import net.sergeych.toread.fb2.Fb2Book
import net.sergeych.toread.fb2.Fb2Epigraph
import net.sergeych.toread.fb2.Fb2EpigraphBlock
import net.sergeych.toread.fb2.Fb2Poem
import net.sergeych.toread.fb2.Fb2PoemBlock
import net.sergeych.toread.fb2.Fb2Section
import net.sergeych.toread.fb2.Fb2Stanza
import net.sergeych.toread.fb2.Fb2Text
import net.sergeych.toread.fb2.Fb2TextSpan
@@ -146,6 +151,74 @@ class ReadAloudContentPlanTest {
assertEquals("/б 'z /1.", plan.sentences.single().spokenText)
}
@Test
fun poemVersesAreIncludedInReadAloudPlan() {
val plan = buildReaderContentPlan(
Fb2Book(
title = "Book",
sections = listOf(
Fb2Section(
blocks = listOf(
Fb2Block.Poem(
Fb2Poem(
blocks = listOf(
Fb2PoemBlock.Stanza(
Fb2Stanza(
verses = listOf(
text("Line one."),
text("Line two."),
),
),
),
),
),
),
),
),
),
),
)
assertEquals(listOf("Line one.", "Line two."), plan.sentences.map { it.text })
assertEquals(1, plan.elements.filterIsInstance<ReaderElement.Poem>().size)
}
@Test
fun epigraphsAreIncludedInReaderPlan() {
val plan = buildReaderContentPlan(
Fb2Book(
title = "Book",
bodyEpigraphs = listOf(
Fb2Epigraph(
blocks = listOf(Fb2EpigraphBlock.Paragraph(text("Body quote."))),
textAuthors = listOf(text("Body author.")),
),
),
sections = listOf(
Fb2Section(
blocks = listOf(
Fb2Block.Epigraph(
Fb2Epigraph(
blocks = listOf(Fb2EpigraphBlock.Paragraph(text("Section quote."))),
textAuthors = listOf(text("Section author.")),
),
),
),
),
),
),
)
assertEquals(2, plan.elements.filterIsInstance<ReaderElement.Epigraph>().size)
assertEquals(
listOf("Body quote.", "Body author.", "Section quote.", "Section author."),
plan.sentences.map { it.text },
)
}
private fun paragraph(text: String): Fb2Block.Paragraph =
Fb2Block.Paragraph(Fb2Text(listOf(Fb2TextSpan(text))))
Fb2Block.Paragraph(text(text))
private fun text(text: String): Fb2Text =
Fb2Text(listOf(Fb2TextSpan(text)))
}
@@ -312,9 +312,15 @@ actual suspend fun viewLibraryBookFile(fileId: String): Boolean = withContext(Di
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
openLibraryDatabase().useLibrary { db ->
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
val clusterId = file.bodyClusterId ?: return@useLibrary BookInfoExtras()
val sourceFileName = file.originalFilename ?: file.storageUri?.substringAfterLast(File.separatorChar)
val clusterId = file.bodyClusterId ?: return@useLibrary BookInfoExtras(
sourceFileName = sourceFileName,
sourceFilePath = file.storageUri,
)
val readingPosition = db.readingStates.getForBodyCluster(clusterId)?.anchor?.formatHintsJson?.toReadingPosition()
BookInfoExtras(
sourceFileName = sourceFileName,
sourceFilePath = file.storageUri,
bookmarks = db.bookmarks.listForBodyCluster(clusterId).map {
BookmarkInfo(
title = it.title,