Added ".fb2" file format. (v1.0.1)

This commit is contained in:
acclorite 2024-06-13 15:48:14 +03:00
parent 49567dcade
commit 6e70901429
12 changed files with 252 additions and 7 deletions

View file

@ -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<Book, CoverImage?>? {
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
}
}

View file

@ -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<List<String>> {
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))
}
}

View file

@ -63,6 +63,7 @@ class EpubFileParser @Inject constructor() : FileParser {
translateWhenOpen = false
) to coverImage
} catch (e: Exception) {
e.printStackTrace()
return null
}
}

View file

@ -21,7 +21,7 @@ import javax.inject.Inject
class EpubTextParser @Inject constructor() : TextParser {
override suspend fun parse(file: File): Resource<List<String>> {
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,

View file

@ -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<Book, CoverImage?>? {
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
}
}

View file

@ -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<List<String>> {
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<String>()
val bodyNodes = document.getElementsByTagName("body")
if (bodyNodes.length == 0) {
return Resource.Error(
UIText.StringResource(R.string.error_file_empty)
)
}
val unformattedLines = mutableListOf<String>()
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<String>()
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<Element> {
val list = mutableListOf<Element>()
for (i in 0 until this.length) {
list.add(this.item(i) as Element)
}
return list
}
}

View file

@ -51,6 +51,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
translateWhenOpen = false
) to null
} catch (e: Exception) {
e.printStackTrace()
return null
}
}

View file

@ -14,7 +14,7 @@ import javax.inject.Inject
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
override suspend fun parse(file: File): Resource<List<String>> {
if (!file.name.endsWith(".pdf")) {
if (!file.name.endsWith(".pdf") || !file.exists()) {
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
}

View file

@ -39,6 +39,7 @@ class TxtFileParser @Inject constructor() : FileParser {
translateWhenOpen = false
) to null
} catch (e: Exception) {
e.printStackTrace()
return null
}
}

View file

@ -14,7 +14,7 @@ import javax.inject.Inject
class TxtTextParser @Inject constructor() : TextParser {
override suspend fun parse(file: File): Resource<List<String>> {
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,

View file

@ -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(