support for poetry and epigraphs
This commit is contained in:
1 parent
316b10d015
commit
de04b9cbed
11 files changed
+787
-10
No files matched your search
@@ -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() }
|
||||
Reference in new issue
Block a user