🚀 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:
parent
2e041f0cbb
commit
b8721b6ad7
48 changed files with 503 additions and 276 deletions
|
|
@ -168,4 +168,8 @@ dependencies {
|
||||||
|
|
||||||
// Gson
|
// Gson
|
||||||
implementation("com.google.code.gson:gson:2.11.0")
|
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")
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,14 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
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.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import ua.acclorite.book_story.data.local.room.BookDao
|
import ua.acclorite.book_story.data.local.room.BookDao
|
||||||
|
|
@ -33,6 +41,26 @@ object AppModule {
|
||||||
.create(GithubAPI::class.java)
|
.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
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideBookDao(app: Application): BookDao {
|
fun provideBookDao(app: Application): BookDao {
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,56 @@ package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
import ua.acclorite.book_story.presentation.core.util.clearMarkdown
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DocumentParser @Inject constructor() {
|
class DocumentParser @Inject constructor() {
|
||||||
/**
|
/**
|
||||||
* Parses document to get it's text.
|
* 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> {
|
suspend fun Document.parseDocument(): List<String> {
|
||||||
val lines = mutableListOf<String>()
|
val lines = mutableListOf<String>()
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
body()
|
body().apply {
|
||||||
.select("p")
|
// Remove manual line breaks from all <p>
|
||||||
.apply {
|
select("p").forEach { element ->
|
||||||
forEach { element ->
|
|
||||||
yield()
|
yield()
|
||||||
|
element.html(element.html().replace(Regex("\\n+"), " "))
|
||||||
val cleanedText = element.html().replace(Regex("\\n+"), " ")
|
element.append("\n")
|
||||||
element.html(cleanedText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
yield()
|
||||||
|
|
||||||
body()
|
val formattedLine = line.replace(
|
||||||
.wholeText()
|
Regex("""\*\*\s*(.*?)\s*\*\*"""), "**$1**"
|
||||||
.lines()
|
).replace(
|
||||||
.forEach { line ->
|
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
||||||
yield()
|
).trim()
|
||||||
if (line.isNotBlank()) {
|
|
||||||
lines.add(line.trim())
|
if (formattedLine.clearMarkdown().isNotBlank()) {
|
||||||
|
lines.add(formattedLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -208,7 +208,7 @@ class EpubTextParser @Inject constructor(
|
||||||
|
|
||||||
val chapter = documentParser.run {
|
val chapter = documentParser.run {
|
||||||
Jsoup.parse(content).parseDocument().dropWhile {
|
Jsoup.parse(content).parseDocument().dropWhile {
|
||||||
it == chapterTitle // Remove chapter title if present
|
it.clearMarkdown().lowercase() == chapterTitle.lowercase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chapter.isEmpty()) {
|
if (chapter.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.local.data_store.DataStore
|
import ua.acclorite.book_story.data.local.data_store.DataStore
|
||||||
import ua.acclorite.book_story.data.local.dto.FavoriteDirectoryEntity
|
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.color_preset.ColorPresetMapper
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.parser.FileParser
|
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.parser.TextParser
|
||||||
import ua.acclorite.book_story.data.remote.GithubAPI
|
import ua.acclorite.book_story.data.remote.GithubAPI
|
||||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.BookWithText
|
import ua.acclorite.book_story.domain.model.BookWithText
|
||||||
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
|
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.ColorPreset
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
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_ALL_SETTINGS = "GET ALL SETTINGS, REPOSITORY"
|
||||||
private const val GET_FILES_FROM_DEVICE = "GET FILES FROM DEVICE, 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_UPDATES = "CHECK FOR UPDATES, REPOSITORY"
|
||||||
|
private const val CHECK_FOR_TEXT_UPDATE = "CHECK FOR TEXT UPDATE, REPOSITORY"
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
@ -77,6 +81,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
private val fileParser: FileParser,
|
private val fileParser: FileParser,
|
||||||
private val textParser: TextParser,
|
private val textParser: TextParser,
|
||||||
|
private val markdownParser: MarkdownParser
|
||||||
) : BookRepository {
|
) : BookRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,27 +128,115 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
* Loads text from given path. Should be .txt.
|
* Loads text from given path. Should be .txt.
|
||||||
* Used to get text from book and load Reader.
|
* 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 textFile = File(textPath)
|
||||||
val lines = mutableListOf<String>()
|
val markdownLines = mutableListOf<AnnotatedString>()
|
||||||
|
|
||||||
if (textPath.isBlank() || !textFile.exists() || textFile.extension != "txt") {
|
if (textPath.isBlank() || !textFile.exists() || textFile.extension != "txt") {
|
||||||
Log.w(GET_TEXT, "Failed to load file: $textPath")
|
Log.w(GET_TEXT, "Failed to load file: $textPath")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
BufferedReader(FileReader(textFile)).forEachLine { line ->
|
BufferedReader(FileReader(textFile)).forEachLine { line ->
|
||||||
if (line.isNotBlank()) {
|
if (line.isNotBlank()) {
|
||||||
lines.add(
|
markdownLines.add(
|
||||||
line.trim()
|
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.")
|
Log.i(GET_TEXT, "Successfully loaded text with markdown.")
|
||||||
return lines
|
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.
|
* Insert history in database.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package ua.acclorite.book_story.domain.repository
|
package ua.acclorite.book_story.domain.repository
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.BookWithText
|
import ua.acclorite.book_story.domain.model.BookWithText
|
||||||
import ua.acclorite.book_story.domain.model.BookWithTextAndCover
|
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.ColorPreset
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
|
|
@ -28,7 +29,9 @@ interface BookRepository {
|
||||||
|
|
||||||
suspend fun getBookText(
|
suspend fun getBookText(
|
||||||
textPath: String
|
textPath: String
|
||||||
): List<String>
|
): List<AnnotatedString>
|
||||||
|
|
||||||
|
suspend fun checkTextForUpdate(bookId: Int): Resource<Pair<List<String>, List<Chapter>>?>
|
||||||
|
|
||||||
suspend fun insertBook(
|
suspend fun insertBook(
|
||||||
bookWithTextAndCover: BookWithTextAndCover
|
bookWithTextAndCover: BookWithTextAndCover
|
||||||
|
|
@ -79,8 +82,6 @@ interface BookRepository {
|
||||||
suspend fun getBookFromFile(
|
suspend fun getBookFromFile(
|
||||||
file: File
|
file: File
|
||||||
): NullableBook
|
): NullableBook
|
||||||
|
|
||||||
suspend fun parseText(file: File): Resource<List<ChapterWithText>>
|
|
||||||
/* - - - - - - - - - - - - - - - - - - - - - - */
|
/* - - - - - - - - - - - - - - - - - - - - - - */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetText @Inject constructor(private val repository: BookRepository) {
|
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)
|
return repository.getBookText(textPath = textPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import my.nanihadesuka.compose.ScrollbarSelectionMode
|
import my.nanihadesuka.compose.ScrollbarSelectionMode
|
||||||
import my.nanihadesuka.compose.ScrollbarSettings
|
import my.nanihadesuka.compose.ScrollbarSettings
|
||||||
|
|
@ -457,7 +458,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Raleway"),
|
UIText.StringValue("Raleway"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.raleway_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -465,7 +468,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Open Sans"),
|
UIText.StringValue("Open Sans"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.opensans_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -473,7 +482,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Mulish"),
|
UIText.StringValue("Mulish"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.mulish_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -481,7 +492,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Arimo"),
|
UIText.StringValue("Arimo"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.arimo_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -489,7 +502,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Garamond"),
|
UIText.StringValue("Garamond"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.garamond_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -497,7 +516,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Roboto Serif"),
|
UIText.StringValue("Roboto Serif"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.robotoserif_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -505,7 +530,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Noto Serif"),
|
UIText.StringValue("Noto Serif"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.notoserif_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -513,7 +544,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Noto Sans"),
|
UIText.StringValue("Noto Sans"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.notosans_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -521,7 +558,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Roboto"),
|
UIText.StringValue("Roboto"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.roboto_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -529,7 +568,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Jost"),
|
UIText.StringValue("Jost"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.jost_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -537,7 +578,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Merriweather"),
|
UIText.StringValue("Merriweather"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.merriweather_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -545,7 +592,13 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Montserrat"),
|
UIText.StringValue("Montserrat"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.montserrat_regular),
|
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(
|
FontWithName(
|
||||||
|
|
@ -553,14 +606,17 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Nunito"),
|
UIText.StringValue("Nunito"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.nunito_regular),
|
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(
|
FontWithName(
|
||||||
"roboto_slab",
|
"roboto_slab",
|
||||||
UIText.StringValue("Roboto Slab"),
|
UIText.StringValue("Roboto Slab"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.robotoslab_regular)
|
Font(R.font.robotoslab_regular),
|
||||||
|
Font(R.font.robotoslab_medium, weight = FontWeight.Medium),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
FontWithName(
|
FontWithName(
|
||||||
|
|
@ -568,7 +624,9 @@ private fun provideFonts() = listOf(
|
||||||
UIText.StringValue("Lora"),
|
UIText.StringValue("Lora"),
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.lora_regular),
|
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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ fun Intent.launchActivity(
|
||||||
try {
|
try {
|
||||||
activity.baseContext.startActivity(intent)
|
activity.baseContext.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
error()
|
error()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -64,5 +65,5 @@ fun Modifier.noRippleClickable(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.clearMarkdown(): String {
|
fun String.clearMarkdown(): String {
|
||||||
return replace(Regex("_|\\*\\*"), "").trim()
|
return replace(Regex("_|\\*\\*"), "")
|
||||||
}
|
}
|
||||||
|
|
@ -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.Category
|
||||||
import ua.acclorite.book_story.domain.model.History
|
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.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.DeleteBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.GetBookById
|
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.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.ResetCoverImage
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBook
|
import ua.acclorite.book_story.domain.use_case.UpdateBook
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBookWithText
|
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.domain.util.UIText
|
||||||
import ua.acclorite.book_story.presentation.core.navigation.Screen
|
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.BaseViewModel
|
||||||
import java.io.File
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
@ -54,10 +52,9 @@ class BookInfoViewModel @Inject constructor(
|
||||||
private val insertHistory: InsertHistory,
|
private val insertHistory: InsertHistory,
|
||||||
private val deleteBooks: DeleteBooks,
|
private val deleteBooks: DeleteBooks,
|
||||||
private val getBookById: GetBookById,
|
private val getBookById: GetBookById,
|
||||||
private val getText: GetText,
|
|
||||||
private val canResetCover: CanResetCover,
|
private val canResetCover: CanResetCover,
|
||||||
private val resetCoverImage: ResetCoverImage,
|
private val resetCoverImage: ResetCoverImage,
|
||||||
private val parseText: ParseText
|
private val checkTextForUpdate: CheckTextForUpdate,
|
||||||
) : BaseViewModel<BookInfoState, BookInfoEvent>() {
|
) : BaseViewModel<BookInfoState, BookInfoEvent>() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(BookInfoState())
|
private val _state = MutableStateFlow(BookInfoState())
|
||||||
|
|
@ -468,83 +465,10 @@ class BookInfoViewModel @Inject constructor(
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
val currentBook = _state.value.book.apply {
|
val result = checkTextForUpdate.execute(bookId = _state.value.book.id)
|
||||||
if (!File(filePath).exists()) {
|
when (result) {
|
||||||
onEvent(
|
is Resource.Success -> {
|
||||||
BookInfoEvent.OnShowSnackbar(
|
if (result.data == null) {
|
||||||
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) {
|
|
||||||
onEvent(
|
onEvent(
|
||||||
BookInfoEvent.OnShowSnackbar(
|
BookInfoEvent.OnShowSnackbar(
|
||||||
event.context.getString(R.string.nothing_changed),
|
event.context.getString(R.string.nothing_changed),
|
||||||
|
|
@ -560,14 +484,11 @@ class BookInfoViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
} else {
|
||||||
|
|
||||||
yield()
|
|
||||||
|
|
||||||
onEvent(
|
onEvent(
|
||||||
BookInfoEvent.OnShowConfirmUpdateDialog(
|
BookInfoEvent.OnShowConfirmUpdateDialog(
|
||||||
updatedText = updatedText,
|
updatedText = result.data.first,
|
||||||
updatedChapters = updatedChapters
|
updatedChapters = result.data.second
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -576,6 +497,37 @@ class BookInfoViewModel @Inject constructor(
|
||||||
checkingForUpdate = false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,6 @@ private fun ReaderScreen(lazyListState: LazyListState) {
|
||||||
|
|
||||||
ReaderTextParagraph(
|
ReaderTextParagraph(
|
||||||
line = line,
|
line = line,
|
||||||
context = context,
|
|
||||||
fontFamily = fontFamily,
|
fontFamily = fontFamily,
|
||||||
fontColor = fontColor.value,
|
fontColor = fontColor.value,
|
||||||
lineHeight = lineHeight,
|
lineHeight = lineHeight,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader.components
|
package ua.acclorite.book_story.presentation.screens.reader.components
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyItemScope
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
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.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.LineBreak
|
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.text.style.TextIndent
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.FontWithName
|
import ua.acclorite.book_story.domain.model.FontWithName
|
||||||
import ua.acclorite.book_story.presentation.core.components.LocalReaderViewModel
|
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.
|
* Reader Text Paragraph item.
|
||||||
*
|
*
|
||||||
* @param line Current line.
|
* @param line Current line.
|
||||||
* @param context Context.
|
|
||||||
* @param fontFamily Line's font family.
|
* @param fontFamily Line's font family.
|
||||||
* @param fontColor Line's font color.
|
* @param fontColor Line's font color.
|
||||||
* @param lineHeight Line's line height.
|
* @param lineHeight Line's line height.
|
||||||
|
|
@ -47,8 +51,7 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.reader.data.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyItemScope.ReaderTextParagraph(
|
fun LazyItemScope.ReaderTextParagraph(
|
||||||
line: String,
|
line: AnnotatedString,
|
||||||
context: Context,
|
|
||||||
fontFamily: FontWithName,
|
fontFamily: FontWithName,
|
||||||
fontColor: Color,
|
fontColor: Color,
|
||||||
lineHeight: TextUnit,
|
lineHeight: TextUnit,
|
||||||
|
|
@ -63,14 +66,13 @@ fun LazyItemScope.ReaderTextParagraph(
|
||||||
toolbarHidden: Boolean
|
toolbarHidden: Boolean
|
||||||
) {
|
) {
|
||||||
val onEvent = LocalReaderViewModel.current.onEvent
|
val onEvent = LocalReaderViewModel.current.onEvent
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(horizontal = sidePadding),
|
||||||
horizontal = sidePadding
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = when (textAlignment) {
|
horizontalAlignment = when (textAlignment) {
|
||||||
ReaderTextAlignment.START, ReaderTextAlignment.JUSTIFY -> Alignment.Start
|
ReaderTextAlignment.START, ReaderTextAlignment.JUSTIFY -> Alignment.Start
|
||||||
|
|
@ -78,6 +80,16 @@ fun LazyItemScope.ReaderTextParagraph(
|
||||||
ReaderTextAlignment.END -> Alignment.End
|
ReaderTextAlignment.END -> Alignment.End
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
when (line.text) {
|
||||||
|
"---" -> {
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 3.dp,
|
||||||
|
modifier = Modifier.clip(CircleShape),
|
||||||
|
color = fontColor.copy(0.3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = line,
|
text = line,
|
||||||
modifier = Modifier.then(
|
modifier = Modifier.then(
|
||||||
|
|
@ -89,7 +101,7 @@ fun LazyItemScope.ReaderTextParagraph(
|
||||||
onDoubleClick = {
|
onDoubleClick = {
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnOpenTranslator(
|
ReaderEvent.OnOpenTranslator(
|
||||||
textToTranslate = line,
|
textToTranslate = line.text,
|
||||||
translateWholeParagraph = true,
|
translateWholeParagraph = true,
|
||||||
context = context as ComponentActivity,
|
context = context as ComponentActivity,
|
||||||
noAppsFound = {
|
noAppsFound = {
|
||||||
|
|
@ -131,4 +143,6 @@ fun LazyItemScope.ReaderTextParagraph(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Immutable
|
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.Book
|
||||||
import ua.acclorite.book_story.domain.model.Chapter
|
import ua.acclorite.book_story.domain.model.Chapter
|
||||||
import ua.acclorite.book_story.domain.util.UIText
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
|
@ -10,7 +11,7 @@ import ua.acclorite.book_story.presentation.core.constants.Constants
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ReaderState(
|
data class ReaderState(
|
||||||
val book: Book = Constants.EMPTY_BOOK,
|
val book: Book = Constants.EMPTY_BOOK,
|
||||||
val text: List<String> = emptyList(),
|
val text: List<AnnotatedString> = emptyList(),
|
||||||
val listState: LazyListState = LazyListState(),
|
val listState: LazyListState = LazyListState(),
|
||||||
|
|
||||||
val currentChapter: Chapter? = null,
|
val currentChapter: Chapter? = null,
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
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.GetBookById
|
||||||
import ua.acclorite.book_story.domain.use_case.GetLatestHistory
|
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.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.use_case.UpdateBook
|
||||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||||
import ua.acclorite.book_story.domain.util.Resource
|
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.navigation.Screen
|
||||||
import ua.acclorite.book_story.presentation.core.util.BaseViewModel
|
import ua.acclorite.book_story.presentation.core.util.BaseViewModel
|
||||||
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
@ -48,7 +47,7 @@ class ReaderViewModel @Inject constructor(
|
||||||
private val getText: GetText,
|
private val getText: GetText,
|
||||||
private val getLatestHistory: GetLatestHistory,
|
private val getLatestHistory: GetLatestHistory,
|
||||||
private val getBookById: GetBookById,
|
private val getBookById: GetBookById,
|
||||||
private val parseText: ParseText
|
private val checkTextForUpdate: CheckTextForUpdate
|
||||||
) : BaseViewModel<ReaderState, ReaderEvent>() {
|
) : BaseViewModel<ReaderState, ReaderEvent>() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ReaderState())
|
private val _state = MutableStateFlow(ReaderState())
|
||||||
|
|
@ -421,43 +420,10 @@ class ReaderViewModel @Inject constructor(
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
val currentBook = _state.value.book.apply {
|
val result = checkTextForUpdate.execute(bookId = _state.value.book.id)
|
||||||
if (!File(filePath).exists()) {
|
when (result) {
|
||||||
_state.update {
|
is Resource.Success -> {
|
||||||
it.copy(
|
if (result.data == null) {
|
||||||
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) {
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
event.noUpdateFound()
|
event.noUpdateFound()
|
||||||
}
|
}
|
||||||
|
|
@ -468,8 +434,7 @@ class ReaderViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
} else {
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
updateFound = true,
|
updateFound = true,
|
||||||
|
|
@ -477,6 +442,20 @@ class ReaderViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onEvent(ReaderEvent.OnShowHideUpdateDialog(true))
|
onEvent(ReaderEvent.OnShowHideUpdateDialog(true))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Resource.Error -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
checkingForUpdate = false,
|
||||||
|
updateFound = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
app/src/main/res/font/arimo_medium.ttf
Normal file
BIN
app/src/main/res/font/arimo_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/arimo_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/arimo_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/garamond_medium.ttf
Normal file
BIN
app/src/main/res/font/garamond_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/garamond_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/garamond_medium_italic.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/src/main/res/font/jost_medium.ttf
Normal file
BIN
app/src/main/res/font/jost_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/jost_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/jost_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/lora_medium.ttf
Normal file
BIN
app/src/main/res/font/lora_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/lora_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/lora_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/merriweather_medium.ttf
Normal file
BIN
app/src/main/res/font/merriweather_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/merriweather_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/merriweather_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/montserrat_medium.ttf
Normal file
BIN
app/src/main/res/font/montserrat_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/montserrat_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/montserrat_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/mulish_medium.ttf
Normal file
BIN
app/src/main/res/font/mulish_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/mulish_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/mulish_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/notosans_medium.ttf
Normal file
BIN
app/src/main/res/font/notosans_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/notosans_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/notosans_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/notoserif_medium.ttf
Normal file
BIN
app/src/main/res/font/notoserif_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/notoserif_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/notoserif_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/nunito_medium.ttf
Normal file
BIN
app/src/main/res/font/nunito_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/nunito_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/nunito_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/opensans_medium.ttf
Normal file
BIN
app/src/main/res/font/opensans_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/opensans_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/opensans_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/raleway_medium.ttf
Normal file
BIN
app/src/main/res/font/raleway_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/raleway_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/raleway_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/roboto_medium.ttf
Normal file
BIN
app/src/main/res/font/roboto_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/roboto_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/roboto_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/robotoserif_medium.ttf
Normal file
BIN
app/src/main/res/font/robotoserif_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/robotoserif_medium_italic.ttf
Normal file
BIN
app/src/main/res/font/robotoserif_medium_italic.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/robotoslab_medium.ttf
Normal file
BIN
app/src/main/res/font/robotoslab_medium.ttf
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue