From 6e70901429a3a0c62b5d44dcfa82d5641a157467 Mon Sep 17 00:00:00 2001 From: acclorite Date: Thu, 13 Jun 2024 15:48:14 +0300 Subject: [PATCH] Added ".fb2" file format. (v1.0.1) --- app/build.gradle.kts | 3 + .../book_story/data/parser/FileParserImpl.kt | 10 +- .../book_story/data/parser/TextParserImpl.kt | 8 +- .../data/parser/epub/EpubFileParser.kt | 1 + .../data/parser/epub/EpubTextParser.kt | 3 +- .../data/parser/fb2/Fb2FileParser.kt | 89 ++++++++++++ .../data/parser/fb2/Fb2TextParser.kt | 136 ++++++++++++++++++ .../data/parser/pdf/PdfFileParser.kt | 1 + .../data/parser/pdf/PdfTextParser.kt | 2 +- .../data/parser/txt/TxtFileParser.kt | 1 + .../data/parser/txt/TxtTextParser.kt | 3 +- .../book_story/domain/util/Constants.kt | 2 +- 12 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd1efada..6dfa0884 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,9 @@ dependencies { implementation("org.slf4j:slf4j-android:latest.release") implementation("org.jsoup:jsoup:latest.release") + // Fb2 parser + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:latest.release") + // App Compat for Language Switcher implementation("androidx.appcompat:appcompat:latest.release") implementation("androidx.appcompat:appcompat-resources:latest.release") diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt index 069ba80d..f497ab85 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -1,6 +1,7 @@ package ua.acclorite.book_story.data.parser import ua.acclorite.book_story.data.parser.epub.EpubFileParser +import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser import ua.acclorite.book_story.domain.model.Book @@ -11,10 +12,11 @@ import javax.inject.Inject class FileParserImpl @Inject constructor( private val txtFileParser: TxtFileParser, private val pdfFileParser: PdfFileParser, - private val epubFileParser: EpubFileParser + private val epubFileParser: EpubFileParser, + private val fb2FileParser: Fb2FileParser ) : FileParser { override suspend fun parse(file: File): Pair? { - val fileFormat = ".${file.name.substringAfterLast(".")}" + val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim() if (fileFormat == ".pdf") { return pdfFileParser.parse(file) @@ -28,6 +30,10 @@ class FileParserImpl @Inject constructor( return txtFileParser.parse(file) } + if (fileFormat == ".fb2") { + return fb2FileParser.parse(file) + } + return null } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt index be99e317..30a8ffee 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -2,6 +2,7 @@ package ua.acclorite.book_story.data.parser import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.epub.EpubTextParser +import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.domain.util.Resource @@ -13,9 +14,10 @@ class TextParserImpl @Inject constructor( private val txtTextParser: TxtTextParser, private val pdfTextParser: PdfTextParser, private val epubTextParser: EpubTextParser, + private val fb2TextParser: Fb2TextParser ) : TextParser { override suspend fun parse(file: File): Resource> { - val fileFormat = ".${file.name.substringAfterLast(".")}" + val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim() if (fileFormat == ".pdf") { return pdfTextParser.parse(file) @@ -29,6 +31,10 @@ class TextParserImpl @Inject constructor( return txtTextParser.parse(file) } + if (fileFormat == ".fb2") { + return fb2TextParser.parse(file) + } + return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index d86c98f3..10471853 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -63,6 +63,7 @@ class EpubFileParser @Inject constructor() : FileParser { translateWhenOpen = false ) to coverImage } catch (e: Exception) { + e.printStackTrace() return null } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index 7f54836b..cff97c0b 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -21,7 +21,7 @@ import javax.inject.Inject class EpubTextParser @Inject constructor() : TextParser { override suspend fun parse(file: File): Resource> { - if (!file.name.endsWith(".epub")) { + if (!file.name.endsWith(".epub") || !file.exists()) { return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } @@ -87,6 +87,7 @@ class EpubTextParser @Inject constructor() : TextParser { return Resource.Success(strings) } catch (e: Exception) { + e.printStackTrace() return Resource.Error( UIText.StringResource( R.string.error_query, diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt new file mode 100644 index 00000000..c7441d46 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt @@ -0,0 +1,89 @@ +package ua.acclorite.book_story.data.parser.fb2 + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.w3c.dom.Document +import org.w3c.dom.Element +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.FileParser +import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.model.Category +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.UIText +import java.io.File +import javax.inject.Inject +import javax.xml.parsers.DocumentBuilderFactory + +class Fb2FileParser @Inject constructor() : FileParser { + + override suspend fun parse(file: File): Pair? { + if (!file.name.endsWith(".fb2") || !file.exists()) { + return null + } + + try { + val factory = DocumentBuilderFactory.newInstance() + val builder = factory.newDocumentBuilder() + val document = withContext(Dispatchers.IO) { + builder.parse(file) + } + + val titleFromFile = extractElementContent(document, "book-title") + val title = titleFromFile ?: file.name.dropLast(4).trim() + + val authorFirstName = extractElementContent(document, "first-name") + val authorLastName = extractElementContent(document, "last-name") + val authorFromFile = StringBuilder() + + if (authorFirstName != null) { + authorFromFile.append( + "$authorFirstName " + ) + } + if (authorLastName != null) { + authorFromFile.append( + authorLastName + ) + } + + val author = if (authorFromFile.isNotBlank()) { + UIText.StringValue(authorFromFile.toString().trim()) + } else { + UIText.StringResource(R.string.unknown_author) + } + + val descriptionFromFile = extractElementContent(document, "annotation") + + return Book( + title = title, + author = author, + description = descriptionFromFile, + textPath = "", + scrollIndex = 0, + scrollOffset = 0, + progress = 0f, + filePath = file.path, + lastOpened = null, + category = Category.entries[0], + coverImage = null, + enableTranslator = false, + translateFrom = "", + translateTo = "", + doubleClickTranslation = false, + translateWhenOpen = false + ) to null + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + private fun extractElementContent(document: Document, tagName: String): String? { + val nodeList = document.getElementsByTagName(tagName) + if (nodeList.length > 0) { + val element = nodeList.item(0) as Element + return element.textContent.trim() + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt new file mode 100644 index 00000000..60694cc8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt @@ -0,0 +1,136 @@ +package ua.acclorite.book_story.data.parser.fb2 + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.w3c.dom.Element +import org.w3c.dom.NodeList +import ua.acclorite.book_story.R +import ua.acclorite.book_story.data.parser.TextParser +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText +import java.io.File +import javax.inject.Inject +import javax.xml.parsers.DocumentBuilderFactory + +class Fb2TextParser @Inject constructor() : TextParser { + + override suspend fun parse(file: File): Resource> { + if (!file.name.endsWith(".fb2") || !file.exists()) { + return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) + } + + return try { + val factory = DocumentBuilderFactory.newInstance() + val builder = factory.newDocumentBuilder() + val document = withContext(Dispatchers.IO) { + builder.parse(file) + } + + val formattedLines = mutableListOf() + + val bodyNodes = document.getElementsByTagName("body") + if (bodyNodes.length == 0) { + return Resource.Error( + UIText.StringResource(R.string.error_file_empty) + ) + } + + val unformattedLines = mutableListOf() + val bodyNode = bodyNodes.item(0) as Element + val paragraphNodes = bodyNode.getElementsByTagName("p") + for (element in paragraphNodes.asList()) { + if (element.textContent.isBlank()) { + continue + } + + unformattedLines.add( + element.textContent.trim() + ) + } + + val lines = mutableListOf() + unformattedLines.forEachIndexed { index, string -> + try { + val line = string.trim() + + if (index == 0) { + lines.add(line) + return@forEachIndexed + } + + if ( + line.all { + if (it == ' ') { + true + } else if (it.isUpperCase() || it.isDigit() || it == '-') { + true + } else { + false + } + } + ) { + return@forEachIndexed + } + + if (line.all { it.isDigit() }) { + return@forEachIndexed + } + + if (line.first().isLowerCase()) { + val currentLine = lines[lines.lastIndex] + + if (currentLine.last() == '-') { + if (currentLine[currentLine.lastIndex - 1].isLowerCase()) { + lines[lines.lastIndex] = currentLine.dropLast(1) + line + return@forEachIndexed + } + } + + lines[lines.lastIndex] += " $line" + return@forEachIndexed + } + + if (line.first().isUpperCase() || line.first().isDigit()) { + lines.add(line) + return@forEachIndexed + } + + if (line.first().isLetter()) { + lines[lines.lastIndex] += " $line" + return@forEachIndexed + } + + } catch (e: Exception) { + e.printStackTrace() + return@forEachIndexed + } + } + + lines.forEach { line -> + formattedLines.add(line.trim()) + } + + if (formattedLines.isEmpty()) { + return Resource.Error(UIText.StringResource(R.string.error_file_empty)) + } + + Resource.Success(formattedLines) + } catch (e: Exception) { + e.printStackTrace() + Resource.Error( + UIText.StringResource( + R.string.error_query, + e.message?.take(40)?.trim() ?: "" + ) + ) + } + } + + private fun NodeList.asList(): List { + val list = mutableListOf() + for (i in 0 until this.length) { + list.add(this.item(i) as Element) + } + return list + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index b8ad38cc..5901cea4 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -51,6 +51,7 @@ class PdfFileParser @Inject constructor(private val application: Application) : translateWhenOpen = false ) to null } catch (e: Exception) { + e.printStackTrace() return null } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt index 77a60472..4bcb33fe 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class PdfTextParser @Inject constructor(private val application: Application) : TextParser { override suspend fun parse(file: File): Resource> { - if (!file.name.endsWith(".pdf")) { + if (!file.name.endsWith(".pdf") || !file.exists()) { return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index b71d3d3e..c865a466 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -39,6 +39,7 @@ class TxtFileParser @Inject constructor() : FileParser { translateWhenOpen = false ) to null } catch (e: Exception) { + e.printStackTrace() return null } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt index 3f47ab8f..4a42beac 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class TxtTextParser @Inject constructor() : TextParser { override suspend fun parse(file: File): Resource> { - if (!file.name.endsWith(".txt")) { + if (!file.name.endsWith(".txt") || !file.exists()) { return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format)) } @@ -37,6 +37,7 @@ class TxtTextParser @Inject constructor() : TextParser { Resource.Success(formattedLines) } catch (e: Exception) { + e.printStackTrace() Resource.Error( UIText.StringResource( R.string.error_query, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt index d717ba21..a69115b2 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt @@ -19,7 +19,7 @@ object Constants { const val MAIN_STATE = "main_state" // Supported file extensions - val EXTENSIONS = listOf(".txt", ".pdf", ".epub") + val EXTENSIONS = listOf(".txt", ".pdf", ".epub", ".fb2") // Supported languages val LANGUAGES = listOf(