Support more formats (#112)

* Added support for FB2 book format

* Add support for CBZ files by introducing a `ReaderDocument` abstraction.

Key changes:
- Defined `ReaderDocument`, `ReaderPage`, and `ReaderTextPage` interfaces to provide a unified API for different document types.
- Implemented `PdfDocumentWrapper` and `CbzDocumentWrapper` to handle PDF and CBZ files respectively.
- Updated `PdfViewerScreen`, `PdfPageComposable`, and `MainViewModel` to use the new unified document interfaces.
- Added CBZ file type detection and cover generation logic.

* Add support for CBR and CB7 comic book formats

- Add `me.zhanghai.android.libarchive` dependency to support RAR and 7z archives.
- Implement `ArchiveDocumentWrapper` using `libarchive` to handle CBZ, CBR, and CB7 files uniformly, replacing the previous CBZ-only implementation.
- Update `MainViewModel` and `DocumentFactory` to recognize and process `.cbr` and `.cb7` extensions and their associated MIME types.
- Add support for extracting and caching cover images from CBR and CB7 archives.
- Update UI components (`HomeScreen`, `LibraryScreen`, `AppNavigation`) to handle the new comic book file types.
This commit is contained in:
Aryan 2026-03-26 12:27:56 +05:30 committed by GitHub
parent 32e29dfc07
commit 4db2c97a30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 871 additions and 93 deletions

View file

@ -256,6 +256,7 @@ import com.aryan.reader.AiDefinitionPopup
import com.aryan.reader.AiDefinitionResult
import com.aryan.reader.BuildConfig
import com.aryan.reader.DeviceVoiceSettingsSheet
import com.aryan.reader.FileType
import com.aryan.reader.MainViewModel
import com.aryan.reader.R
import com.aryan.reader.SearchResult
@ -289,7 +290,6 @@ import com.aryan.reader.tts.splitTextIntoChunks
import io.legere.pdfiumandroid.api.Bookmark
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import io.legere.pdfiumandroid.suspend.PdfPageKt
import io.legere.pdfiumandroid.suspend.PdfTextPageKt
import io.legere.pdfiumandroid.suspend.PdfiumCoreKt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -972,9 +972,9 @@ private fun saveTtsMode(context: Context, mode: TtsPlaybackManager.TtsMode) {
prefs.edit { putString(TTS_MODE_KEY, mode.name) }
}
private suspend fun renderPageToBitmap(doc: PdfDocumentKt, pageIndex: Int): Bitmap? {
private suspend fun renderPageToBitmap(doc: ReaderDocument, pageIndex: Int): Bitmap? {
return withContext(Dispatchers.IO) {
var page: PdfPageKt? = null
var page: ReaderPage? = null
try {
page = doc.openPage(pageIndex)
if (page == null) return@withContext null
@ -1425,7 +1425,7 @@ fun PdfViewerScreen(
)
else null
}
var pdfDocument by remember { mutableStateOf<PdfDocumentKt?>(null) }
var pdfDocument by remember { mutableStateOf<ReaderDocument?>(null) }
var pfdState by remember { mutableStateOf<ParcelFileDescriptor?>(null) }
var totalPages by remember { mutableIntStateOf(0) }
var currentPageScale by remember { mutableFloatStateOf(1f) }
@ -2276,10 +2276,13 @@ fun PdfViewerScreen(
if (extractedText.isBlank() && currentBookId != null && pdfDocument != null) {
Timber.d("Extracted text is blank. Attempting repository/OCR fallback...")
try {
extractedText = pdfTextRepository.getOrExtractText(
currentBookId!!, pdfDocument!!, pageIndex
)
Timber.d("Repository: Extracted text length: ${extractedText.length}")
val pdfDocKt = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument
if (pdfDocKt != null) {
extractedText = pdfTextRepository.getOrExtractText(
currentBookId!!, pdfDocKt, pageIndex
)
Timber.d("Repository: Extracted text length: ${extractedText.length}")
}
} catch (e: Exception) {
Timber.w(e, "Bookmark: Repository extraction failed")
}
@ -2571,11 +2574,12 @@ fun PdfViewerScreen(
val onGetOcrSearchRectsStable = remember(pdfTextRepository, pdfDocument) {
val callback: suspend (Int, String) -> List<RectF> = { page, query ->
if (pdfDocument != null) {
val hasNative = pdfTextRepository.hasNativeText(pdfDocument!!, page)
val pdfDocKt = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument
if (pdfDocKt != null) {
val hasNative = pdfTextRepository.hasNativeText(pdfDocKt, page)
if (!hasNative) {
pdfTextRepository.getOcrSearchRects(
document = pdfDocument!!,
document = pdfDocKt,
pageIndex = page,
query = query,
onModelDownloading = { isOcrModelDownloading = true })
@ -2759,9 +2763,9 @@ fun PdfViewerScreen(
coroutineScope.launch {
val pageToRead = pageToReadOverride ?: currentPage
var rawPageText: String? = null
var tempPage: PdfPageKt? = null
var tempTextPage: PdfTextPageKt? = null
var ocrAttempted = false
var tempPage: ReaderPage? = null
var tempTextPage: ReaderTextPage? = null
@Suppress("CanBeVal") var ocrAttempted = false
try {
withContext(Dispatchers.IO) {
@ -2816,8 +2820,8 @@ fun PdfViewerScreen(
val chunks = splitTextIntoChunks(textToChunk)
val bookTitle = pdfDocument?.getDocumentMeta()?.title?.takeIf { it.isNotBlank() }
?: pdfUri.lastPathSegment ?: "PDF Document"
val bookTitle = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument?.getDocumentMeta()?.title?.takeIf { it.isNotBlank() }
?: pdfUri.lastPathSegment ?: "Document"
val pageTitle = "Page ${pageToRead + 1}"
val ttsChunks = chunks.mapIndexed { index, text -> TtsChunk(text, "", index) }
@ -3035,7 +3039,7 @@ fun PdfViewerScreen(
currentPfdOpened = context.contentResolver.openFileDescriptor(pdfUri, "r")
if (currentPfdOpened == null) throw Exception("Failed to open ParcelFileDescriptor")
val doc = pdfiumCore.newDocument(currentPfdOpened, documentPassword)
val doc = DocumentFactory.loadDocument(context, pdfUri, uiState.selectedFileType ?: FileType.PDF, documentPassword, pdfiumCore)
if (!isActive) {
doc.close()
@ -3049,7 +3053,7 @@ fun PdfViewerScreen(
if (pagesCount > 0) {
try {
val tableOfContents = doc.getFixedTableOfContents()
val tableOfContents = doc.getTableOfContents()
val flattened = flattenToc(tableOfContents)
withContext(Dispatchers.Main) { flatTableOfContents = flattened }
} catch (e: Exception) {
@ -3264,8 +3268,8 @@ fun PdfViewerScreen(
LaunchedEffect(pdfUri, currentBookId, totalPages) {
if (currentBookId == null || totalPages == 0) return@LaunchedEffect
if (isBackgroundIndexing && backgroundIndexingProgress > 0f) return@LaunchedEffect
if (uiState.selectedFileType != FileType.PDF) return@LaunchedEffect
withContext(Dispatchers.IO) {
val storedLang = pdfTextRepository.getBookLanguage(currentBookId!!)
@ -4110,9 +4114,10 @@ fun PdfViewerScreen(
Timber.d(
"LaunchedEffect triggered for Page $pageIndex. Checking Native..."
)
val hasNative = pdfTextRepository.hasNativeText(
pdfDocument!!, pageIndex
)
val pdfDocKt = (pdfDocument as? PdfDocumentWrapper)?.pdfDocument
val hasNative = if (pdfDocKt != null) pdfTextRepository.hasNativeText(
pdfDocKt, pageIndex
) else false
Timber.d(
"Page $pageIndex Has Native Text: $hasNative"
)
@ -4121,13 +4126,13 @@ fun PdfViewerScreen(
Timber.d(
"Fetching OCR rects for query: '${target.query}'"
)
val rects = pdfTextRepository.getOcrSearchRects(
document = pdfDocument!!,
val rects = if (pdfDocKt != null) pdfTextRepository.getOcrSearchRects(
document = pdfDocKt,
pageIndex = pageIndex,
query = target.query,
onModelDownloading = {
isOcrModelDownloading = true
})
}) else emptyList()
Timber.d(
"Received ${rects.size} rects from Repository."
)
@ -5533,31 +5538,32 @@ fun PdfViewerScreen(
Icons.Default.Share, contentDescription = null
)
})
DropdownMenuItem(
text = { Text("Save copy to device") },
onClick = {
showMoreMenu = false
showSaveDialog = true
},
leadingIcon = {
Icon(
Icons.Default.Save, contentDescription = null
)
})
DropdownMenuItem(
text = { Text("Print") },
onClick = {
if (uiState.selectedFileType == FileType.PDF) {
DropdownMenuItem(
text = { Text("Save copy to device") },
onClick = {
showMoreMenu = false
showSaveDialog = true
},
leadingIcon = {
Icon(
Icons.Default.Save,
contentDescription = null
)
})
}
if (uiState.selectedFileType == FileType.PDF) {
DropdownMenuItem(text = { Text("Print") }, onClick = {
showMoreMenu = false
onPrintDocument()
},
leadingIcon = {
}, leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.print),
contentDescription = null,
modifier = Modifier.size(20.dp)
)
}
)
})
}
}
}
}