More formats support (#161)
* Added support for ODT and FODT file formats. * Improved `OdtParser` to support enhanced document formatting and external assets * Added support for viewing CSV, TSV, JSON, XML, logs, and source code files by dynamically converting them to HTML.
This commit is contained in:
parent
6a60aec0ef
commit
291504fd90
8 changed files with 652 additions and 76 deletions
|
|
@ -136,7 +136,7 @@ enum class AddBooksSource(val displayName: String) {
|
|||
}
|
||||
|
||||
enum class FileType {
|
||||
PDF, EPUB, MOBI, MD, TXT, HTML, FB2, CBZ, CBR, CB7, DOCX
|
||||
PDF, EPUB, MOBI, MD, TXT, HTML, FB2, CBZ, CBR, CB7, DOCX, ODT, FODT
|
||||
}
|
||||
|
||||
enum class RenderMode {
|
||||
|
|
@ -254,6 +254,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
private val epubParser = EpubParser(appContext)
|
||||
private val mobiParser = MobiParser(appContext)
|
||||
private val fb2Parser = com.aryan.reader.epub.Fb2Parser(appContext)
|
||||
private val odtParser = com.aryan.reader.epub.OdtParser(appContext)
|
||||
private val singleFileImporter = SingleFileImporter(appContext)
|
||||
private val bookImporter = BookImporter(appContext)
|
||||
private val prefs: SharedPreferences =
|
||||
|
|
@ -2536,7 +2537,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
var author: String? = null
|
||||
var bookForMetadata = epubBook
|
||||
|
||||
if (bookForMetadata == null && (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX)) {
|
||||
if (bookForMetadata == null && (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX || type == FileType.ODT || type == FileType.FODT)) {
|
||||
Timber.d("Parsing downloaded book for cover/metadata: $displayName")
|
||||
Timber.tag("FileOpenPerf")
|
||||
.d("[$bookId] addFileToRecent: Starting metadata parsing (no book provided)")
|
||||
|
|
@ -2570,6 +2571,15 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
parseContent = false
|
||||
)
|
||||
}
|
||||
FileType.ODT, FileType.FODT -> {
|
||||
odtParser.createOdtBook(
|
||||
inputStream = inputStream,
|
||||
bookId = bookId,
|
||||
originalBookNameHint = displayName,
|
||||
isFlat = type == FileType.FODT,
|
||||
parseContent = false
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
singleFileImporter.importSingleFile(
|
||||
inputStream,
|
||||
|
|
@ -2598,7 +2608,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
|
||||
val finalBookMetadata = bookForMetadata
|
||||
|
||||
if ((type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX) && finalBookMetadata != null) {
|
||||
if ((type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX || type == FileType.ODT || type == FileType.FODT) && finalBookMetadata != null) {
|
||||
title =
|
||||
finalBookMetadata.title.takeIf { it.isNotBlank() && it != "content" } ?: displayName
|
||||
|
||||
|
|
@ -3203,7 +3213,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
Timber.tag("FileSwitch").d("PDF state updated, suppressing navigation event for smooth transition")
|
||||
}
|
||||
}
|
||||
} else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX) {
|
||||
} else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX || type == FileType.ODT || type == FileType.FODT) {
|
||||
viewModelScope.launch {
|
||||
val recentItem = recentFilesRepository.getFileByBookId(bookId)
|
||||
if (recentItem?.sourceFolderUri != null) {
|
||||
|
|
@ -3251,7 +3261,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
FileType.FB2 -> {
|
||||
loadFb2(uri, bookId, customDisplayName = originalDisplayName)
|
||||
}
|
||||
|
||||
FileType.ODT, FileType.FODT -> {
|
||||
loadOdt(uri, bookId, type == FileType.FODT, customDisplayName = originalDisplayName)
|
||||
}
|
||||
else -> {
|
||||
loadSingleFile(
|
||||
uri, bookId, type, customDisplayName = originalDisplayName
|
||||
|
|
@ -3299,6 +3311,43 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadOdt(uri: Uri, bookId: String, isFlat: Boolean, customDisplayName: String? = null) {
|
||||
val loadStart = System.currentTimeMillis()
|
||||
Timber.tag("FileOpenPerf").d("[$bookId] loadOdt START | isFlat=$isFlat")
|
||||
viewModelScope.launch {
|
||||
if (!_internalState.value.isLoading) {
|
||||
_internalState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||
}
|
||||
Timber.d("Starting ODT parsing for URI: $uri")
|
||||
try {
|
||||
val odtBook = withContext(Dispatchers.IO) {
|
||||
appContext.contentResolver.openInputStream(uri).use { inputStream ->
|
||||
if (inputStream == null) throw Exception("Could not open input stream")
|
||||
odtParser.createOdtBook(
|
||||
inputStream = inputStream,
|
||||
bookId = bookId,
|
||||
originalBookNameHint = customDisplayName ?: getFileNameFromUri(uri, appContext) ?: if (isFlat) "unknown.fodt" else "unknown.odt",
|
||||
isFlat = isFlat
|
||||
)
|
||||
}
|
||||
}
|
||||
Timber.i("ODT parsing successful. Title: ${odtBook.title}")
|
||||
Timber.tag("FileOpenPerf").d("[$bookId] loadOdt completed | chapters=${odtBook.chapters.size} | elapsed=${System.currentTimeMillis() - loadStart}ms")
|
||||
|
||||
addFileToRecent(
|
||||
uri, if (isFlat) FileType.FODT else FileType.ODT, bookId, odtBook, customDisplayName, isRecent = true, sourceFolderUri = null
|
||||
)
|
||||
|
||||
_internalState.update { it.copy(selectedEpubBook = odtBook, isLoading = false) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error parsing ODT for URI: $uri")
|
||||
_internalState.update {
|
||||
it.copy(errorMessage = appContext.getString(R.string.error_load_file, e.message), isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSingleFile(
|
||||
uri: Uri,
|
||||
bookId: String,
|
||||
|
|
@ -3366,6 +3415,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
Timber.d("Determining type for: $uri | Mime: $mimeType | Name: $fileName")
|
||||
|
||||
return when (mimeType) {
|
||||
"application/vnd.oasis.opendocument.text" -> FileType.ODT
|
||||
"application/x-vnd.oasis.opendocument.text-flat-xml" -> FileType.FODT
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> FileType.DOCX
|
||||
"application/zip", "application/vnd.comicbook+zip", "application/x-cbz" -> {
|
||||
if (fileName?.endsWith(".cbz", ignoreCase = true) == true) FileType.CBZ else null
|
||||
|
|
@ -3382,13 +3433,26 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
"application/x-mobipocket-ebook", "application/vnd.amazon.ebook", "application/vnd.amazon.mobi8-ebook" -> FileType.MOBI
|
||||
"text/markdown", "text/x-markdown" -> FileType.MD
|
||||
"text/html", "application/xhtml+xml" -> FileType.HTML
|
||||
|
||||
"text/csv", "text/comma-separated-values", "text/tab-separated-values",
|
||||
"application/json", "application/xml", "text/xml",
|
||||
"text/x-java-source", "text/x-python", "text/x-kotlin",
|
||||
"text/javascript", "application/javascript",
|
||||
"text/x-c", "text/x-c++", "text/x-csharp", "text/x-ruby", "text/x-go", "text/x-log" -> FileType.HTML
|
||||
|
||||
"text/plain" -> {
|
||||
if (fileName?.endsWith(
|
||||
".md",
|
||||
ignoreCase = true
|
||||
) == true || fileName?.endsWith(".markdown", ignoreCase = true) == true
|
||||
) {
|
||||
if (fileName?.endsWith(".md", ignoreCase = true) == true || fileName?.endsWith(".markdown", ignoreCase = true) == true) {
|
||||
FileType.MD
|
||||
} else if (fileName?.let {
|
||||
it.endsWith(".csv", ignoreCase = true) || it.endsWith(".tsv", ignoreCase = true) ||
|
||||
it.endsWith(".json", ignoreCase = true) || it.endsWith(".xml", ignoreCase = true) ||
|
||||
it.endsWith(".log", ignoreCase = true) || it.endsWith(".java", ignoreCase = true) ||
|
||||
it.endsWith(".kt", ignoreCase = true) || it.endsWith(".py", ignoreCase = true) ||
|
||||
it.endsWith(".js", ignoreCase = true) || it.endsWith(".cpp", ignoreCase = true) ||
|
||||
it.endsWith(".c", ignoreCase = true) || it.endsWith(".cs", ignoreCase = true) ||
|
||||
it.endsWith(".rb", ignoreCase = true) || it.endsWith(".go", ignoreCase = true)
|
||||
} == true) {
|
||||
FileType.HTML
|
||||
} else {
|
||||
FileType.TXT
|
||||
}
|
||||
|
|
@ -3439,6 +3503,22 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
|||
ignoreCase = true
|
||||
) == true -> FileType.HTML
|
||||
fileName?.endsWith(".docx", ignoreCase = true) == true -> FileType.DOCX
|
||||
fileName?.endsWith(".odt", ignoreCase = true) == true -> FileType.ODT
|
||||
fileName?.endsWith(".fodt", ignoreCase = true) == true -> FileType.FODT
|
||||
fileName?.endsWith(".csv", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".tsv", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".json", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".xml", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".log", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".java", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".kt", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".py", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".js", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".cpp", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".c", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".cs", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".rb", ignoreCase = true) == true ||
|
||||
fileName?.endsWith(".go", ignoreCase = true) == true -> FileType.HTML
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue