General improvements (#134)

* Add visual options and seamless transitions to the EPUB reader

* Add support for DOCX file format

* Refine padding and status bar inset handling in Epub pagination mode

* Add file size parameter for recent files and sorting

* Refactor book data cleanup logic into a centralized method

* Bump version to 1.0.40(41)
This commit is contained in:
Aryan 2026-03-30 22:44:26 +05:30 committed by GitHub
parent 15264a31ae
commit 6fd6dd5609
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 623 additions and 134 deletions

View file

@ -136,7 +136,7 @@ enum class AddBooksSource(val displayName: String) {
}
enum class FileType {
PDF, EPUB, MOBI, MD, TXT, HTML, FB2, CBZ, CBR, CB7
PDF, EPUB, MOBI, MD, TXT, HTML, FB2, CBZ, CBR, CB7, DOCX
}
enum class RenderMode {
@ -161,9 +161,13 @@ data class Shelf(val name: String, val books: List<RecentFileItem>) {
}
enum class SortOrder(val displayName: String) {
RECENT("Recent"), TITLE_ASC("Title A-Z"), AUTHOR_ASC("Author A-Z"), PERCENT_ASC("Percent complete 0-100"), PERCENT_DESC(
"Percent complete 100-0"
)
RECENT("Recent"),
TITLE_ASC("Title A-Z"),
AUTHOR_ASC("Author A-Z"),
PERCENT_ASC("Percent complete 0-100"),
PERCENT_DESC("Percent complete 100-0"),
SIZE_ASC("Size (Smallest)"),
SIZE_DESC("Size (Biggest)")
}
enum class ReadStatusFilter(val displayName: String) {
@ -372,6 +376,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
SortOrder.AUTHOR_ASC -> files.sortedWith(compareBy(nullsLast()) { it.author?.lowercase() })
SortOrder.PERCENT_ASC -> files.sortedBy { it.progressPercentage ?: 0f }
SortOrder.PERCENT_DESC -> files.sortedByDescending { it.progressPercentage ?: 0f }
SortOrder.SIZE_ASC -> files.sortedBy { it.fileSize }
SortOrder.SIZE_DESC -> files.sortedByDescending { it.fileSize }
}
}
@ -478,8 +484,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (filesToDelete.isNotEmpty()) {
val ids = filesToDelete.map { it.bookId }
ids.forEach { bookId ->
pdfTextRepository.clearBookText(bookId)
clearImportedFileCache(bookId)
cleanupBookDataLocally(bookId)
try {
val cacheDir = File(appContext.cacheDir, "opds_stream_${bookId.hashCode()}")
if (cacheDir.exists()) cacheDir.deleteRecursively()
@ -838,14 +843,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.d("Deleting book permanently from reader: $bookId")
pdfTextRepository.clearBookText(bookId)
try {
val cacheDir = File(appContext.cacheDir, "imported_file_$bookId")
if (cacheDir.exists()) cacheDir.deleteRecursively()
} catch (e: Exception) {
Timber.e(e, "Failed to clear cache for $bookId")
}
cleanupBookDataLocally(bookId)
recentFilesRepository.deleteFilePermanently(listOf(bookId))
withContext(Dispatchers.Main) {
@ -1013,13 +1011,20 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private fun getFastFileId(context: Context, uri: Uri): String {
var result = uri.toString()
try {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val size = if (sizeIndex != -1) cursor.getLong(sizeIndex) else 0L
val name = if (nameIndex != -1) cursor.getString(nameIndex) else "unknown"
result = "${name}_${size}"
if (uri.scheme == "file") {
uri.path?.let {
val file = File(it)
result = "${file.name}_${file.length()}"
}
} else {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val size = if (sizeIndex != -1) cursor.getLong(sizeIndex) else 0L
val name = if (nameIndex != -1) cursor.getString(nameIndex) else "unknown"
result = "${name}_${size}"
}
}
}
} catch (e: Exception) {
@ -1560,7 +1565,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { it.copy(syncedFolders = currentFolders) }
val filesToRemove = recentFilesRepository.getFilesBySourceFolder(folder.uriString)
filesToRemove.forEach { pdfTextRepository.clearBookText(it.bookId) }
filesToRemove.forEach { cleanupBookDataLocally(it.bookId) }
recentFilesRepository.deleteFilesBySourceFolder(folder.uriString)
try {
@ -1671,8 +1676,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val idsToRemove = filesToRemove.map { it.bookId }
idsToRemove.forEach { bookId ->
pdfTextRepository.clearBookText(bookId)
clearImportedFileCache(bookId)
cleanupBookDataLocally(bookId)
}
recentFilesRepository.deleteFilePermanently(idsToRemove)
}
@ -1689,7 +1693,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val folders = _internalState.value.syncedFolders
folders.forEach { folder ->
val filesToRemove = recentFilesRepository.getFilesBySourceFolder(folder.uriString)
filesToRemove.forEach { pdfTextRepository.clearBookText(it.bookId) }
filesToRemove.forEach { cleanupBookDataLocally(it.bookId) }
recentFilesRepository.deleteFilesBySourceFolder(folder.uriString)
try {
@ -1902,6 +1906,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
viewModelScope.launch {
try {
recentFilesRepository.clearAllLocalData()
clearBookCache()
pdfTextRepository.clearAllText()
pdfTextBoxRepository.clearAll()
prefs.edit { remove(KEY_LAST_SYNC_TIMESTAMP) }
@ -2378,6 +2383,24 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
recentFilesRepository.getFileByBookId(bookId) == null
}
val fileSize = withContext(Dispatchers.IO) {
try {
if (uri.scheme == "file") {
uri.path?.let { File(it).length() } ?: 0L
} else {
appContext.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (sizeIndex != -1) cursor.getLong(sizeIndex) else 0L
} else 0L
} ?: 0L
}
} catch (e: Exception) {
Timber.e(e, "Failed to get file size for $uri")
0L
}
}
val existingItem = recentFilesRepository.getFileByBookId(bookId)
val displayName = customDisplayName ?: existingItem?.displayName ?: getFileNameFromUri(
uri, appContext
@ -2388,7 +2411,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)) {
if (bookForMetadata == null && (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX)) {
Timber.d("Parsing downloaded book for cover/metadata: $displayName")
Timber.tag("FileOpenPerf")
.d("[$bookId] addFileToRecent: Starting metadata parsing (no book provided)")
@ -2450,7 +2473,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) && finalBookMetadata != null) {
if ((type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX) && finalBookMetadata != null) {
title =
finalBookMetadata.title.takeIf { it.isNotBlank() && it != "content" } ?: displayName
@ -2529,7 +2552,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
lastModifiedTimestamp = newLastModifiedTimestamp,
isDeleted = false,
isRecent = isRecent,
sourceFolderUri = sourceFolderUri
sourceFolderUri = sourceFolderUri,
fileSize = fileSize
)
recentFilesRepository.addRecentFile(newItem)
Timber.i("Added/Updated $displayName ($type) to recent files via repository.")
@ -2871,6 +2895,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
private suspend fun cleanupBookDataLocally(bookId: String) {
pdfTextRepository.clearBookText(bookId)
clearImportedFileCache(bookId)
bookCacheDao.deleteEntireBookCache(bookId)
}
private fun clearImportedFileCache(bookId: String) {
try {
val cacheDir = File(appContext.cacheDir, "imported_file_$bookId")
@ -2892,15 +2922,24 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (uri.scheme != "opds-pse") {
try {
val cursor = appContext.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val size = if (sizeIndex != -1) it.getLong(sizeIndex) else -1L
val name = if (nameIndex != -1) it.getString(nameIndex) else "unknown"
Timber.tag("FileOpenPerf")
.d("[$bookId] File details | name=$name | size=${size} bytes | sizeMB=${size / (1024.0 * 1024)}")
if (uri.scheme == "file") {
uri.path?.let {
val file = File(it)
val size = file.length()
val name = file.name
Timber.tag("FileOpenPerf").d("[$bookId] File details | name=$name | size=${size} bytes | sizeMB=${size / (1024.0 * 1024)}")
}
} else {
val cursor = appContext.contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val size = if (sizeIndex != -1) it.getLong(sizeIndex) else -1L
val name = if (nameIndex != -1) it.getString(nameIndex) else "unknown"
Timber.tag("FileOpenPerf")
.d("[$bookId] File details | name=$name | size=${size} bytes | sizeMB=${size / (1024.0 * 1024)}")
}
}
}
} catch (e: Exception) {
@ -2952,7 +2991,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
sourceFolderUri = null
)
}
} else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML) {
} else if (type == FileType.EPUB || type == FileType.MOBI || type == FileType.FB2 || type == FileType.MD || type == FileType.TXT || type == FileType.HTML || type == FileType.DOCX) {
viewModelScope.launch {
val recentItem = recentFilesRepository.getFileByBookId(bookId)
if (recentItem?.sourceFolderUri != null) {
@ -3110,6 +3149,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.d("Determining type for: $uri | Mime: $mimeType | Name: $fileName")
return when (mimeType) {
"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
}
@ -3181,6 +3221,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
".htm",
ignoreCase = true
) == true -> FileType.HTML
fileName?.endsWith(".docx", ignoreCase = true) == true -> FileType.DOCX
else -> null
}
@ -3846,7 +3887,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
folderBooks.forEach { item ->
idsToDeleteLocally.add(item.bookId)
pdfTextRepository.clearBookText(item.bookId)
cleanupBookDataLocally(item.bookId)
clearImportedFileCache(item.bookId)
@ -3912,8 +3953,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
for (item in managedBooks) {
recentFilesRepository.markAsDeleted(listOf(item.bookId))
pdfTextRepository.clearBookText(item.bookId)
clearImportedFileCache(item.bookId)
cleanupBookDataLocally(item.bookId)
firestoreRepository.syncBookMetadata(
currentUser.uid,
@ -3941,8 +3981,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.e(e, "Error during permanent deletion")
recentFilesRepository.deleteFilePermanently(managedBooks.map { it.bookId })
managedBooks.forEach { item ->
clearImportedFileCache(item.bookId)
pdfTextRepository.clearBookText(item.bookId)
cleanupBookDataLocally(item.bookId)
}
_internalState.update {
it.copy(
@ -3954,8 +3993,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} else {
recentFilesRepository.deleteFilePermanently(managedBooks.map { it.bookId })
managedBooks.forEach { item ->
clearImportedFileCache(item.bookId)
pdfTextRepository.clearBookText(item.bookId)
cleanupBookDataLocally(item.bookId)
}
}
}
@ -4073,8 +4111,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val reflowBookIds = reflowBooks.map { it.bookId }
reflowBookIds.forEach { bookId ->
clearImportedFileCache(bookId)
pdfTextRepository.clearBookText(bookId)
cleanupBookDataLocally(bookId)
}
recentFilesRepository.deleteFilePermanently(reflowBookIds)