🚀 Markdown support in Reader

* Added Markdown support: Bold, Italic, Links and Section separator (hr)
* Added new font variations (Bold & Bold Italic)
* Improve text update (moved to repository & logged)

Resolves: #93
This commit is contained in:
Acclorite 2024-10-01 15:10:44 +03:00
parent 2e041f0cbb
commit b8721b6ad7
48 changed files with 503 additions and 276 deletions

View file

@ -168,4 +168,8 @@ dependencies {
// Gson
implementation("com.google.code.gson:gson:2.11.0")
// Markdown
implementation("org.commonmark:commonmark:0.23.0")
implementation("org.commonmark:commonmark-ext-autolink:0.23.0")
}

View file

@ -8,6 +8,14 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.node.BlockQuote
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.Heading
import org.commonmark.node.HtmlBlock
import org.commonmark.node.IndentedCodeBlock
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import ua.acclorite.book_story.data.local.room.BookDao
@ -33,6 +41,26 @@ object AppModule {
.create(GithubAPI::class.java)
}
@Provides
@Singleton
fun provideCommonmarkParser(): Parser {
return Parser
.builder()
.extensions(listOf(AutolinkExtension.create()))
.enabledBlockTypes(
setOf(
Heading::class.java,
HtmlBlock::class.java,
ThematicBreak::class.java,
BlockQuote::class.java,
FencedCodeBlock::class.java,
IndentedCodeBlock::class.java,
ThematicBreak::class.java
)
)
.build()
}
@Provides
@Singleton
fun provideBookDao(app: Application): BookDao {

View file

@ -2,41 +2,56 @@ package ua.acclorite.book_story.data.parser
import kotlinx.coroutines.yield
import org.jsoup.nodes.Document
import ua.acclorite.book_story.presentation.core.util.clearMarkdown
import javax.inject.Inject
class DocumentParser @Inject constructor() {
/**
* Parses document to get it's text.
* Fixes issues such as manual line breaking in <p>.
* Applies Markdown to the text: Bold(**), Italic(_), Section separator(---), and Links(a > href).
*
* @return Parsed text line by line.
* @return Parsed text line by line with Markdown(all lines are not blank).
*/
suspend fun Document.parseDocument(): List<String> {
val lines = mutableListOf<String>()
yield()
body()
.select("p")
.apply {
forEach { element ->
body().apply {
// Remove manual line breaks from all <p>
select("p").forEach { element ->
yield()
val cleanedText = element.html().replace(Regex("\\n+"), " ")
element.html(cleanedText)
element.html(element.html().replace(Regex("\\n+"), " "))
element.append("\n")
}
append("\n")
// Markdown
select("hr").append("\n---\n")
select("b").append("**").prepend("**")
select("h1").append("**").prepend("**")
select("h2").append("**").prepend("**")
select("h3").append("**").prepend("**")
select("strong").append("**").prepend("**")
select("em").append("_").prepend("_")
select("a").forEach { element ->
val link = element.attr("href")
if (!link.startsWith("http") || element.wholeText().isBlank()) return@forEach
element.prepend("[")
element.append("](${element.attr("href")})")
}
}.wholeText().lines().forEach { line ->
yield()
body()
.wholeText()
.lines()
.forEach { line ->
yield()
if (line.isNotBlank()) {
lines.add(line.trim())
val formattedLine = line.replace(
Regex("""\*\*\s*(.*?)\s*\*\*"""), "**$1**"
).replace(
Regex("""_\s*(.*?)\s*_"""), "_$1_"
).trim()
if (formattedLine.clearMarkdown().isNotBlank()) {
lines.add(formattedLine)
}
}

View file

@ -0,0 +1,92 @@
package ua.acclorite.book_story.data.parser
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import org.commonmark.node.Emphasis
import org.commonmark.node.Heading
import org.commonmark.node.Link
import org.commonmark.node.Node
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.parser.Parser
import ua.acclorite.book_story.presentation.core.util.clearMarkdown
import javax.inject.Inject
class MarkdownParser @Inject constructor(
private val commonmarkParser: Parser
) {
/**
* Parses markdown text to [androidx.compose.ui.text.AnnotatedString].
*
* @return Parsed annotated string.
*/
fun parse(markdown: String): AnnotatedString {
return try {
val annotatedString = buildAnnotatedString {
parseNode(commonmarkParser.parse(markdown))
}.ifBlank { buildAnnotatedString { append(markdown) } }
.trim() as AnnotatedString
annotatedString
} catch (e: Exception) {
e.printStackTrace()
buildAnnotatedString { append(markdown) }
}
}
/**
* Parses [Node].
* Appends text and applies styles to the target [AnnotatedString.Builder].
*/
private fun AnnotatedString.Builder.parseNode(node: Node) {
when (node) {
is Heading, is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
parseChildren(node)
}
}
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
parseChildren(node)
}
}
is Link -> {
withLink(
LinkAnnotation.Url(
node.destination,
styles = TextLinkStyles(style = SpanStyle(textDecoration = TextDecoration.Underline))
)
) {
parseChildren(node)
}
}
is Text -> {
append(node.literal.clearMarkdown())
parseChildren(node)
}
else -> {
parseChildren(node)
}
}
}
private fun AnnotatedString.Builder.parseChildren(node: Node) {
var child = node.firstChild
while (child != null) {
parseNode(child)
child = child.next
}
}
}

View file

@ -208,7 +208,7 @@ class EpubTextParser @Inject constructor(
val chapter = documentParser.run {
Jsoup.parse(content).parseDocument().dropWhile {
it == chapterTitle // Remove chapter title if present
it.clearMarkdown().lowercase() == chapterTitle.lowercase()
}
}
if (chapter.isEmpty()) {

View file

@ -7,12 +7,14 @@ import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import androidx.compose.ui.text.AnnotatedString
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.dto.FavoriteDirectoryEntity
@ -22,13 +24,14 @@ import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.MarkdownParser
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.remote.GithubAPI
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
import ua.acclorite.book_story.domain.model.ChapterWithText
import ua.acclorite.book_story.domain.model.Chapter
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
@ -60,6 +63,7 @@ private const val RESET_COVER = "RESET COVER, REPOSITORY"
private const val GET_ALL_SETTINGS = "GET ALL SETTINGS, REPOSITORY"
private const val GET_FILES_FROM_DEVICE = "GET FILES FROM DEVICE, REPOSITORY"
private const val CHECK_FOR_UPDATES = "CHECK FOR UPDATES, REPOSITORY"
private const val CHECK_FOR_TEXT_UPDATE = "CHECK FOR TEXT UPDATE, REPOSITORY"
@Suppress("DEPRECATION")
@Singleton
@ -77,6 +81,7 @@ class BookRepositoryImpl @Inject constructor(
private val fileParser: FileParser,
private val textParser: TextParser,
private val markdownParser: MarkdownParser
) : BookRepository {
/**
@ -123,27 +128,115 @@ class BookRepositoryImpl @Inject constructor(
* Loads text from given path. Should be .txt.
* Used to get text from book and load Reader.
*/
override suspend fun getBookText(textPath: String): List<String> {
override suspend fun getBookText(textPath: String): List<AnnotatedString> {
val textFile = File(textPath)
val lines = mutableListOf<String>()
val markdownLines = mutableListOf<AnnotatedString>()
if (textPath.isBlank() || !textFile.exists() || textFile.extension != "txt") {
Log.w(GET_TEXT, "Failed to load file: $textPath")
return emptyList()
}
try {
withContext(Dispatchers.IO) {
BufferedReader(FileReader(textFile)).forEachLine { line ->
if (line.isNotBlank()) {
lines.add(
line.trim()
markdownLines.add(
markdownParser.parse(line.trim())
)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(GET_TEXT, "Could not get text with markdown.")
return emptyList()
}
Log.i(GET_TEXT, "Successfully loaded text.")
return lines
Log.i(GET_TEXT, "Successfully loaded text with markdown.")
return markdownLines
}
/**
* Checks whether the text of the book([bookId]) is up-to-date and did not change.
*
* @return If [Resource.Success] and returns null, then the book is up-to-date.
*/
override suspend fun checkTextForUpdate(bookId: Int): Resource<Pair<List<String>, List<Chapter>>?> {
yield()
try {
val book = database.findBookById(bookId)
Log.i(CHECK_FOR_TEXT_UPDATE, "Checking [${book.title}] for text update.")
yield()
val text = withContext(Dispatchers.IO) {
val lines = mutableListOf<String>()
BufferedReader(FileReader(book.textPath)).forEachLine { line ->
if (line.isNotBlank()) {
lines.add(line.trim())
}
}
lines.toList()
}
val chapters = book.chapters
Log.i(CHECK_FOR_TEXT_UPDATE, "Got current text and chapters.")
yield()
val bookFile = File(book.filePath).apply {
if (!exists()) {
Log.e(CHECK_FOR_TEXT_UPDATE, "Couldn't get book's file: does not exist.")
return Resource.Error(
message = UIText.StringResource(
R.string.file_not_found,
name.takeLast(50)
)
)
}
}
yield()
val (updatedText, updatedChapters) = textParser.parse(bookFile).run {
if (this is Resource.Error) {
Log.e(CHECK_FOR_TEXT_UPDATE, "Couldn't get updated book's text.")
return Resource.Error(
message = UIText.StringResource(
R.string.error_file_empty
)
)
}
data!!.map { it.text }.flatten() to data.map { it.chapter }.run {
if (size < 2) return@run emptyList()
return@run this
}
}
Log.i(CHECK_FOR_TEXT_UPDATE, "Successfully got new text and chapters.")
yield()
return Resource.Success(
if (updatedText == text && updatedChapters == chapters) {
Log.i(CHECK_FOR_TEXT_UPDATE, "Text is up-to-date(${book.title}).")
null
} else {
Log.i(CHECK_FOR_TEXT_UPDATE, "Found difference in ${book.title}.")
updatedText to updatedChapters
}
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(CHECK_FOR_TEXT_UPDATE, "Check failed with the error: ${e.message}")
return Resource.Error(
UIText.StringResource(
R.string.error_query,
e.message ?: ""
)
)
}
}
/**
@ -700,16 +793,6 @@ class BookRepositoryImpl @Inject constructor(
)
}
/**
* Parse text from given [file].
* May give [Resource.Error] if something went wrong.
*
* @param file File to parse. Should be one of supported file formats.
*/
override suspend fun parseText(file: File): Resource<List<ChapterWithText>> {
return textParser.parse(file)
}
/**
* Insert history in database.
*/

View file

@ -1,11 +1,12 @@
package ua.acclorite.book_story.domain.repository
import androidx.compose.ui.text.AnnotatedString
import androidx.datastore.preferences.core.Preferences
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
import ua.acclorite.book_story.domain.model.ChapterWithText
import ua.acclorite.book_story.domain.model.Chapter
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
@ -28,7 +29,9 @@ interface BookRepository {
suspend fun getBookText(
textPath: String
): List<String>
): List<AnnotatedString>
suspend fun checkTextForUpdate(bookId: Int): Resource<Pair<List<String>, List<Chapter>>?>
suspend fun insertBook(
bookWithTextAndCover: BookWithTextAndCover
@ -79,8 +82,6 @@ interface BookRepository {
suspend fun getBookFromFile(
file: File
): NullableBook
suspend fun parseText(file: File): Resource<List<ChapterWithText>>
/* - - - - - - - - - - - - - - - - - - - - - - */

View file

@ -0,0 +1,13 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Chapter
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject
class CheckTextForUpdate @Inject constructor(private val repository: BookRepository) {
suspend fun execute(bookId: Int): Resource<Pair<List<String>, List<Chapter>>?> {
return repository.checkTextForUpdate(bookId)
}
}

View file

@ -1,11 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import androidx.compose.ui.text.AnnotatedString
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(textPath: String): List<String> {
suspend fun execute(textPath: String): List<AnnotatedString> {
return repository.getBookText(textPath = textPath)
}
}

View file

@ -1,14 +0,0 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ChapterWithText
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Resource
import java.io.File
import javax.inject.Inject
class ParseText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(file: File): Resource<List<ChapterWithText>> {
return repository.parseText(file)
}
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import my.nanihadesuka.compose.ScrollbarSelectionMode
import my.nanihadesuka.compose.ScrollbarSettings
@ -457,7 +458,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Raleway"),
FontFamily(
Font(R.font.raleway_regular),
Font(R.font.raleway_regular_italic, style = FontStyle.Italic)
Font(R.font.raleway_regular_italic, style = FontStyle.Italic),
Font(R.font.raleway_medium, weight = FontWeight.Medium),
Font(R.font.raleway_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
@ -465,7 +468,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Open Sans"),
FontFamily(
Font(R.font.opensans_regular),
Font(R.font.opensans_regular_italic, style = FontStyle.Italic)
Font(R.font.opensans_regular_italic, style = FontStyle.Italic),
Font(R.font.opensans_medium, weight = FontWeight.Medium),
Font(
R.font.opensans_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -473,7 +482,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Mulish"),
FontFamily(
Font(R.font.mulish_regular),
Font(R.font.mulish_regular_italic, style = FontStyle.Italic)
Font(R.font.mulish_regular_italic, style = FontStyle.Italic),
Font(R.font.mulish_medium, weight = FontWeight.Medium),
Font(R.font.mulish_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
@ -481,7 +492,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Arimo"),
FontFamily(
Font(R.font.arimo_regular),
Font(R.font.arimo_regular_italic, style = FontStyle.Italic)
Font(R.font.arimo_regular_italic, style = FontStyle.Italic),
Font(R.font.arimo_medium, weight = FontWeight.Medium),
Font(R.font.arimo_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
@ -489,7 +502,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Garamond"),
FontFamily(
Font(R.font.garamond_regular),
Font(R.font.garamond_regular_italic, style = FontStyle.Italic)
Font(R.font.garamond_regular_italic, style = FontStyle.Italic),
Font(R.font.garamond_medium, weight = FontWeight.Medium),
Font(
R.font.garamond_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -497,7 +516,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Roboto Serif"),
FontFamily(
Font(R.font.robotoserif_regular),
Font(R.font.robotoserif_regular_italic, style = FontStyle.Italic)
Font(R.font.robotoserif_regular_italic, style = FontStyle.Italic),
Font(R.font.robotoserif_medium, weight = FontWeight.Medium),
Font(
R.font.robotoserif_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -505,7 +530,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Noto Serif"),
FontFamily(
Font(R.font.notoserif_regular),
Font(R.font.notoserif_regular_italic, style = FontStyle.Italic)
Font(R.font.notoserif_regular_italic, style = FontStyle.Italic),
Font(R.font.notoserif_medium, weight = FontWeight.Medium),
Font(
R.font.notoserif_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -513,7 +544,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Noto Sans"),
FontFamily(
Font(R.font.notosans_regular),
Font(R.font.notosans_regular_italic, style = FontStyle.Italic)
Font(R.font.notosans_regular_italic, style = FontStyle.Italic),
Font(R.font.notosans_medium, weight = FontWeight.Medium),
Font(
R.font.notosans_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -521,7 +558,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Roboto"),
FontFamily(
Font(R.font.roboto_regular),
Font(R.font.roboto_regular_italic, style = FontStyle.Italic)
Font(R.font.roboto_regular_italic, style = FontStyle.Italic),
Font(R.font.roboto_medium, weight = FontWeight.Medium),
Font(R.font.roboto_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
@ -529,7 +568,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Jost"),
FontFamily(
Font(R.font.jost_regular),
Font(R.font.jost_regular_italic, style = FontStyle.Italic)
Font(R.font.jost_regular_italic, style = FontStyle.Italic),
Font(R.font.jost_medium, weight = FontWeight.Medium),
Font(R.font.jost_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
@ -537,7 +578,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Merriweather"),
FontFamily(
Font(R.font.merriweather_regular),
Font(R.font.merriweather_regular_italic, style = FontStyle.Italic)
Font(R.font.merriweather_regular_italic, style = FontStyle.Italic),
Font(R.font.merriweather_medium, weight = FontWeight.Medium),
Font(
R.font.merriweather_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -545,7 +592,13 @@ private fun provideFonts() = listOf(
UIText.StringValue("Montserrat"),
FontFamily(
Font(R.font.montserrat_regular),
Font(R.font.montserrat_regular_italic, style = FontStyle.Italic)
Font(R.font.montserrat_regular_italic, style = FontStyle.Italic),
Font(R.font.montserrat_medium, weight = FontWeight.Medium),
Font(
R.font.montserrat_medium_italic,
weight = FontWeight.Medium,
style = FontStyle.Italic
)
)
),
FontWithName(
@ -553,14 +606,17 @@ private fun provideFonts() = listOf(
UIText.StringValue("Nunito"),
FontFamily(
Font(R.font.nunito_regular),
Font(R.font.nunito_regular_italic, style = FontStyle.Italic)
Font(R.font.nunito_regular_italic, style = FontStyle.Italic),
Font(R.font.nunito_medium, weight = FontWeight.Medium),
Font(R.font.nunito_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
),
FontWithName(
"roboto_slab",
UIText.StringValue("Roboto Slab"),
FontFamily(
Font(R.font.robotoslab_regular)
Font(R.font.robotoslab_regular),
Font(R.font.robotoslab_medium, weight = FontWeight.Medium),
)
),
FontWithName(
@ -568,7 +624,9 @@ private fun provideFonts() = listOf(
UIText.StringValue("Lora"),
FontFamily(
Font(R.font.lora_regular),
Font(R.font.lora_regular_italic, style = FontStyle.Italic)
Font(R.font.lora_regular_italic, style = FontStyle.Italic),
Font(R.font.lora_medium, weight = FontWeight.Medium),
Font(R.font.lora_medium_italic, weight = FontWeight.Medium, style = FontStyle.Italic)
)
)
)

View file

@ -31,6 +31,7 @@ fun Intent.launchActivity(
try {
activity.baseContext.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
error()
return
}
@ -64,5 +65,5 @@ fun Modifier.noRippleClickable(
}
fun String.clearMarkdown(): String {
return replace(Regex("_|\\*\\*"), "").trim()
return replace(Regex("_|\\*\\*"), "")
}

View file

@ -26,11 +26,10 @@ import ua.acclorite.book_story.domain.model.BookWithText
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.use_case.CanResetCover
import ua.acclorite.book_story.domain.use_case.CheckTextForUpdate
import ua.acclorite.book_story.domain.use_case.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBookById
import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.ParseText
import ua.acclorite.book_story.domain.use_case.ResetCoverImage
import ua.acclorite.book_story.domain.use_case.UpdateBook
import ua.acclorite.book_story.domain.use_case.UpdateBookWithText
@ -40,7 +39,6 @@ import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.core.navigation.Screen
import ua.acclorite.book_story.presentation.core.util.BaseViewModel
import java.io.File
import java.util.Date
import javax.inject.Inject
import kotlin.math.roundToInt
@ -54,10 +52,9 @@ class BookInfoViewModel @Inject constructor(
private val insertHistory: InsertHistory,
private val deleteBooks: DeleteBooks,
private val getBookById: GetBookById,
private val getText: GetText,
private val canResetCover: CanResetCover,
private val resetCoverImage: ResetCoverImage,
private val parseText: ParseText
private val checkTextForUpdate: CheckTextForUpdate,
) : BaseViewModel<BookInfoState, BookInfoEvent>() {
private val _state = MutableStateFlow(BookInfoState())
@ -468,83 +465,10 @@ class BookInfoViewModel @Inject constructor(
yield()
val currentBook = _state.value.book.apply {
if (!File(filePath).exists()) {
onEvent(
BookInfoEvent.OnShowSnackbar(
text = event.context.getString(
R.string.file_not_found,
filePath.substringAfterLast("/").takeLast(25)
),
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
)
},
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
checkingForUpdate = false
)
}
return@launch
}
}
val updatedBook = parseText.execute(File(currentBook.filePath)).apply {
if (this is Resource.Error || data == null) {
onEvent(
BookInfoEvent.OnShowSnackbar(
text = message?.asString(event.context)
?: event.context.getString(
R.string.error_something_went_wrong_with_file
),
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
)
},
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
checkingForUpdate = false
)
}
return@launch
}
}.data!!
yield()
val updatedText = updatedBook.map { it.text }.flatten()
val text = getText.execute(currentBook.textPath)
val updatedChapters = updatedBook.map { it.chapter }.run {
if (size < 2) return@run emptyList()
return@run this
}
val chapters = currentBook.chapters
yield()
if (updatedText == text && updatedChapters == chapters) {
val result = checkTextForUpdate.execute(bookId = _state.value.book.id)
when (result) {
is Resource.Success -> {
if (result.data == null) {
onEvent(
BookInfoEvent.OnShowSnackbar(
event.context.getString(R.string.nothing_changed),
@ -560,14 +484,11 @@ class BookInfoViewModel @Inject constructor(
)
}
return@launch
}
yield()
} else {
onEvent(
BookInfoEvent.OnShowConfirmUpdateDialog(
updatedText = updatedText,
updatedChapters = updatedChapters
updatedText = result.data.first,
updatedChapters = result.data.second
)
)
@ -576,6 +497,37 @@ class BookInfoViewModel @Inject constructor(
checkingForUpdate = false
)
}
return@launch
}
}
is Resource.Error -> {
onEvent(
BookInfoEvent.OnShowSnackbar(
text = result.message?.asString(event.context) ?: "",
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
)
},
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
checkingForUpdate = false
)
}
return@launch
}
}
}
}

View file

@ -494,7 +494,6 @@ private fun ReaderScreen(lazyListState: LazyListState) {
ReaderTextParagraph(
line = line,
context = context,
fontFamily = fontFamily,
fontColor = fontColor.value,
lineHeight = lineHeight,

View file

@ -1,17 +1,21 @@
package ua.acclorite.book_story.presentation.screens.reader.components
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.LineBreak
@ -19,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.presentation.core.components.LocalReaderViewModel
@ -31,7 +36,6 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.
* Reader Text Paragraph item.
*
* @param line Current line.
* @param context Context.
* @param fontFamily Line's font family.
* @param fontColor Line's font color.
* @param lineHeight Line's line height.
@ -47,8 +51,7 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.
*/
@Composable
fun LazyItemScope.ReaderTextParagraph(
line: String,
context: Context,
line: AnnotatedString,
fontFamily: FontWithName,
fontColor: Color,
lineHeight: TextUnit,
@ -63,14 +66,13 @@ fun LazyItemScope.ReaderTextParagraph(
toolbarHidden: Boolean
) {
val onEvent = LocalReaderViewModel.current.onEvent
val context = LocalContext.current
Column(
modifier = Modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.fillMaxWidth()
.padding(
horizontal = sidePadding
),
.padding(horizontal = sidePadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = when (textAlignment) {
ReaderTextAlignment.START, ReaderTextAlignment.JUSTIFY -> Alignment.Start
@ -78,6 +80,16 @@ fun LazyItemScope.ReaderTextParagraph(
ReaderTextAlignment.END -> Alignment.End
}
) {
when (line.text) {
"---" -> {
HorizontalDivider(
thickness = 3.dp,
modifier = Modifier.clip(CircleShape),
color = fontColor.copy(0.3f)
)
}
else -> {
BasicText(
text = line,
modifier = Modifier.then(
@ -89,7 +101,7 @@ fun LazyItemScope.ReaderTextParagraph(
onDoubleClick = {
onEvent(
ReaderEvent.OnOpenTranslator(
textToTranslate = line,
textToTranslate = line.text,
translateWholeParagraph = true,
context = context as ComponentActivity,
noAppsFound = {
@ -132,3 +144,5 @@ fun LazyItemScope.ReaderTextParagraph(
)
}
}
}
}

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Chapter
import ua.acclorite.book_story.domain.util.UIText
@ -10,7 +11,7 @@ import ua.acclorite.book_story.presentation.core.constants.Constants
@Immutable
data class ReaderState(
val book: Book = Constants.EMPTY_BOOK,
val text: List<String> = emptyList(),
val text: List<AnnotatedString> = emptyList(),
val listState: LazyListState = LazyListState(),
val currentChapter: Chapter? = null,

View file

@ -26,10 +26,10 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.use_case.CheckTextForUpdate
import ua.acclorite.book_story.domain.use_case.GetBookById
import ua.acclorite.book_story.domain.use_case.GetLatestHistory
import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.ParseText
import ua.acclorite.book_story.domain.use_case.UpdateBook
import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.domain.util.Resource
@ -37,7 +37,6 @@ import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.core.navigation.Screen
import ua.acclorite.book_story.presentation.core.util.BaseViewModel
import ua.acclorite.book_story.presentation.core.util.launchActivity
import java.io.File
import javax.inject.Inject
import kotlin.math.roundToInt
@ -48,7 +47,7 @@ class ReaderViewModel @Inject constructor(
private val getText: GetText,
private val getLatestHistory: GetLatestHistory,
private val getBookById: GetBookById,
private val parseText: ParseText
private val checkTextForUpdate: CheckTextForUpdate
) : BaseViewModel<ReaderState, ReaderEvent>() {
private val _state = MutableStateFlow(ReaderState())
@ -421,43 +420,10 @@ class ReaderViewModel @Inject constructor(
yield()
val currentBook = _state.value.book.apply {
if (!File(filePath).exists()) {
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
}
val updatedBook = parseText.execute(File(currentBook.filePath)).apply {
if (this is Resource.Error || data == null) {
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
}.data!!
yield()
val updatedText = updatedBook.map { it.text }.flatten()
val text = getText.execute(currentBook.textPath)
val updatedChapters = updatedBook.map { it.chapter }.run {
if (size < 2) return@run emptyList()
return@run this
}
val chapters = currentBook.chapters
yield()
if (updatedText == text && updatedChapters == chapters) {
val result = checkTextForUpdate.execute(bookId = _state.value.book.id)
when (result) {
is Resource.Success -> {
if (result.data == null) {
withContext(Dispatchers.Main) {
event.noUpdateFound()
}
@ -468,8 +434,7 @@ class ReaderViewModel @Inject constructor(
)
}
return@launch
}
} else {
_state.update {
it.copy(
updateFound = true,
@ -477,6 +442,20 @@ class ReaderViewModel @Inject constructor(
)
}
onEvent(ReaderEvent.OnShowHideUpdateDialog(true))
return@launch
}
}
is Resource.Error -> {
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.