Pdf text reflow (#36)
* Implemented PDF reflow mode by introducing a mechanism to convert PDF content to Markdown/HTML for viewing in the EPUB reader. Specific changes include: - Added `PdfReflowGenerator` and `PdfToMarkdownGenerator` to handle PDF text extraction and conversion to reflowable formats. - Updated `MainViewModel` with `toggleReflowMode` logic to switch between original PDF and reflowed views. - Modified `RecentFileEntity` and `RecentFileDao` to persist user reflow preferences, including a Room database migration (v12 to v13). - Updated `PdfViewerScreen` and `EpubReaderControls` to include UI options for toggling reflow mode. - Integrated reflow preference check into the book opening workflow to automatically load the preferred view. - Updated `AppNavigation` and `EpubReaderScreen` to support the new view switching state. * Implemented background processing and incremental loading for PDF reflow mode. - Added `reflowProgress` to `MainViewModel` to track and display PDF-to-Markdown conversion progress in the UI. - Refactored `PdfToMarkdownGenerator` to generate a skeleton EPUB structure immediately while processing page content (text and images) asynchronously. - Switched PDF text extraction to use `PDFBox` with optimized memory settings and JPEG compression for images. - Implemented priority page processing in reflow mode, starting with the user's current page. - Added "Clear Reflow Cache" debug option to the Home Screen. - Enhanced `BookPaginator` to support lazy loading of chapter content from disk and improved cache hit detection. * Refactored PDF Reflow Mode to generate standalone Markdown files instead of temporary EPUB books. * perf(reflow): optimize PDF-to-Markdown conversion and fix viewing lag - Re-architected PdfToMarkdownGenerator to use a single-pass stream (O(N) complexity), fixing performance bottlenecks and timeouts on large PDFs. - Implemented "Virtual Chaptering" in SingleFileImporter for Markdown files to split content into page-level HTML files, eliminating UI lag during reading. - Simplified ReflowWorker to delegate progress tracking and looping to the generator. - Enhanced PdfViewerScreen with a prominent top-bar progress indicator and a completion snackbar with an "OPEN" action. * Optimized EPUB parsing performance and fixed PDF viewer UI layout. - Optimized `EpubParser` by implementing parallel chapter parsing using coroutines and a semaphore to limit concurrency. - Reduced memory usage in `EpubParser` and `SingleFileImporter` by no longer storing full HTML content in memory for chapters. - Updated `EpubXMLFileParser` to support an existing `Document` object to avoid redundant Jsoup parsing. - Fixed an issue in `PdfViewerScreen` where the snackbar was appearing under the bottom app bar.
This commit is contained in:
parent
8f52549c19
commit
1e879eb604
21 changed files with 936 additions and 165 deletions
|
|
@ -151,7 +151,8 @@ fun EpubReaderTopBar(
|
|||
onOpenTtsSettings: () -> Unit,
|
||||
onOpenDeviceVoiceSettings: () -> Unit,
|
||||
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onToggleReflow: (() -> Unit)? = null,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
|
|
@ -201,6 +202,24 @@ fun EpubReaderTopBar(
|
|||
expanded = showMoreMenu,
|
||||
onDismissRequest = { showMoreMenu = false }
|
||||
) {
|
||||
if (onToggleReflow != null) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("View Original PDF") },
|
||||
onClick = {
|
||||
showMoreMenu = false
|
||||
onToggleReflow()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.picture_as_pdf),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Reading Mode: Vertical") },
|
||||
enabled = !isTtsActive,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ import com.aryan.reader.BannerMessage
|
|||
import com.aryan.reader.BuildConfig
|
||||
import com.aryan.reader.CustomTopBanner
|
||||
import com.aryan.reader.DeviceVoiceSettingsSheet
|
||||
import com.aryan.reader.MainViewModel
|
||||
import com.aryan.reader.RenderMode
|
||||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.SummarizationResult
|
||||
|
|
@ -291,8 +292,25 @@ fun EpubReaderScreen(
|
|||
coverImagePath: String?,
|
||||
onRenderModeChange: (RenderMode) -> Unit,
|
||||
customFonts: List<CustomFontEntity>,
|
||||
onImportFont: (Uri) -> Unit
|
||||
onImportFont: (Uri) -> Unit,
|
||||
viewModel: MainViewModel
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
val isReflowFile = uiState.selectedBookId?.endsWith("_reflow") == true
|
||||
val originalBookId = if (isReflowFile) uiState.selectedBookId!!.removeSuffix("_reflow") else null
|
||||
|
||||
val onOpenOriginal: (() -> Unit)? = if (originalBookId != null) {
|
||||
{
|
||||
val originalItem = uiState.recentFiles.find { it.bookId == originalBookId }
|
||||
if (originalItem != null) {
|
||||
viewModel.onRecentFileClicked(originalItem)
|
||||
} else {
|
||||
viewModel.showBanner("Original PDF not found.", true)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
EpubReaderHost(
|
||||
epubBook = epubBook,
|
||||
renderMode = renderMode,
|
||||
|
|
@ -307,7 +325,8 @@ fun EpubReaderScreen(
|
|||
coverImagePath = coverImagePath,
|
||||
onRenderModeChange = onRenderModeChange,
|
||||
customFonts = customFonts,
|
||||
onImportFont = onImportFont
|
||||
onImportFont = onImportFont,
|
||||
onToggleReflow = onOpenOriginal
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +349,8 @@ fun EpubReaderHost(
|
|||
coverImagePath: String?,
|
||||
onRenderModeChange: (RenderMode) -> Unit,
|
||||
customFonts: List<CustomFontEntity>,
|
||||
onImportFont: (Uri) -> Unit
|
||||
onImportFont: (Uri) -> Unit,
|
||||
onToggleReflow: (() -> Unit)? = null
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
|
|
@ -389,7 +409,9 @@ fun EpubReaderHost(
|
|||
|
||||
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
|
||||
|
||||
val bookId = remember(epubBook.title) { getBookIdForPrefs(epubBook.title) }
|
||||
val bookId = remember(epubBook.title, epubBook.fileName) {
|
||||
if (epubBook.fileName.length > 20) epubBook.fileName else getBookIdForPrefs(epubBook.title)
|
||||
}
|
||||
var isAutoScrollLocal by remember { mutableStateOf(loadAutoScrollLocalMode(context, bookId)) }
|
||||
|
||||
val initialSettings = remember(isAutoScrollLocal) {
|
||||
|
|
@ -1008,6 +1030,7 @@ fun EpubReaderHost(
|
|||
chapterChunks = result.chunks
|
||||
isChapterParsing = false
|
||||
loadUpToChunkIndex = result.startChunkIndex
|
||||
Timber.tag("ReflowPaginationDiag").d("EpubReaderScreen: loadChapterContent finished. chapterChunks.size=${chapterChunks.size}, isChapterParsing=$isChapterParsing")
|
||||
|
||||
if (chunkTargetOverride != null) {
|
||||
chunkTargetOverride = null
|
||||
|
|
@ -1030,6 +1053,7 @@ fun EpubReaderHost(
|
|||
|
||||
var isPagerInitialized by remember(initialLocator) { mutableStateOf(initialLocator == null) }
|
||||
LaunchedEffect(paginator, currentRenderMode, isPagerInitialized) {
|
||||
Timber.tag("ReflowPaginationDiag").d("EpubReaderScreen: Checking paginator init. currentRenderMode=$currentRenderMode, paginator=${paginator != null}, isPagerInitialized=$isPagerInitialized")
|
||||
if (currentRenderMode == RenderMode.PAGINATED && paginator != null && !isPagerInitialized) {
|
||||
scope.launch {
|
||||
val bookPaginator = paginator as? BookPaginator
|
||||
|
|
@ -2880,7 +2904,8 @@ fun EpubReaderHost(
|
|||
searchFocusRequester = searchFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
onOpenTtsSettings = { showTtsSettingsSheet = true },
|
||||
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true }
|
||||
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true },
|
||||
onToggleReflow = onToggleReflow,
|
||||
)
|
||||
|
||||
val autoScrollPadding by androidx.compose.animation.core.animateDpAsState(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue