Crash fixes (#38)

* Updated animation specifications for UI controls and overlays to use a 200ms tween duration.

* Implemented fallback file picker and improved CSS rule merging efficiency to fix OOM.
This commit is contained in:
Aryan 2026-03-07 19:40:50 +05:30 committed by GitHub
parent 77a09c545f
commit 3a31e4fc7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 133 additions and 58 deletions

View file

@ -214,11 +214,31 @@ fun HomeScreen(
} }
} }
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
) { uris ->
if (isContextualModeActive) {
viewModel.clearContextualAction()
}
uris.forEach { uri ->
viewModel.onFileSelected(uri, isFromRecent = false)
}
}
val onSelectFileClick = { val onSelectFileClick = {
if (isContextualModeActive) { if (isContextualModeActive) {
viewModel.clearContextualAction() viewModel.clearContextualAction()
} }
try {
pickFileLauncher.launch(arrayOf("*/*")) pickFileLauncher.launch(arrayOf("*/*"))
} catch (_: android.content.ActivityNotFoundException) {
Timber.w("OpenDocument picker failed. Falling back to GetMultipleContents.")
try {
fallbackFilePickerLauncher.launch("*/*")
} catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true)
}
}
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {

View file

@ -109,6 +109,7 @@ import com.aryan.reader.data.RecentFileItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -156,7 +157,14 @@ fun LibraryScreen(
viewModel.setSyncedFolder(it) viewModel.setSyncedFolder(it)
} }
} }
val onSelectSyncFolderClick = { pickFolderLauncher.launch(null) }
val onSelectSyncFolderClick = {
try {
pickFolderLauncher.launch(null)
} catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("Your device doesn't support folder selection. You can still import files individually.", isError = true)
}
}
val pickFileLauncher = rememberFilePickerLauncher { uris -> val pickFileLauncher = rememberFilePickerLauncher { uris ->
if (isContextualModeActive) { if (isContextualModeActive) {
@ -167,11 +175,31 @@ fun LibraryScreen(
} }
} }
val fallbackFilePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
) { uris ->
if (isContextualModeActive) {
viewModel.clearContextualAction()
}
uris.forEach { uri ->
viewModel.onFileSelected(uri, isFromRecent = false)
}
}
val onSelectFileClick = { val onSelectFileClick = {
if (isContextualModeActive) { if (isContextualModeActive) {
viewModel.clearContextualAction() viewModel.clearContextualAction()
} }
try {
pickFileLauncher.launch(arrayOf("*/*")) pickFileLauncher.launch(arrayOf("*/*"))
} catch (_: android.content.ActivityNotFoundException) {
Timber.w("OpenDocument picker failed. Falling back to GetMultipleContents.")
try {
fallbackFilePickerLauncher.launch("*/*")
} catch (_: android.content.ActivityNotFoundException) {
viewModel.showBanner("No file manager found. Please install a file manager app.", isError = true)
}
}
} }
LaunchedEffect(pagerState) { LaunchedEffect(pagerState) {

View file

@ -156,8 +156,8 @@ fun EpubReaderTopBar(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically { -it } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { -it } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { -it } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { -it } + fadeOut(animationSpec = tween(200)),
modifier = modifier modifier = modifier
) { ) {
Surface( Surface(
@ -347,8 +347,8 @@ fun EpubReaderBottomBar(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)),
modifier = modifier modifier = modifier
) { ) {
Surface( Surface(
@ -456,11 +456,10 @@ fun EpubReaderPageSlider(
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = isVisible, visible = isVisible,
enter = slideInVertically { fullHeight -> fullHeight } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { fullHeight -> fullHeight } + fadeOut() exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200))
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
// Dismiss area
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -470,14 +469,6 @@ fun EpubReaderPageSlider(
) { onClose() } ) { onClose() }
) )
// Fast scrub overlay
// Note: In the refactor, we rely on the parent or this logic to determine "isFastScrubbing".
// Since `isFastScrubbing` was state in the parent, we'll implement a local check or just show it if `isVisible`.
// Ideally, the parent handles the "Scrubbing Animation" separately, but let's bundle it here for simplicity.
// For now, we only show the static overlay logic.
// If we want the big center indicator, we can render it based on interaction state here.
// Top back button
IconButton( IconButton(
onClick = onClose, onClick = onClose,
modifier = Modifier modifier = Modifier

View file

@ -2667,8 +2667,8 @@ fun EpubReaderHost(
// Page Info Bar (Vertical) // Page Info Bar (Vertical)
AnimatedVisibility( AnimatedVisibility(
visible = renderMode == RenderMode.VERTICAL_SCROLL && !showBars, visible = renderMode == RenderMode.VERTICAL_SCROLL && !showBars,
enter = fadeIn(), enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(), exit = fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.BottomCenter) modifier = Modifier.align(Alignment.BottomCenter)
) { ) {
Box( Box(
@ -2710,8 +2710,8 @@ fun EpubReaderHost(
// Page Info Bar (Paginated) // Page Info Bar (Paginated)
AnimatedVisibility( AnimatedVisibility(
visible = renderMode == RenderMode.PAGINATED && paginator != null && !showBars && paginatedPagerState.pageCount > 0, visible = renderMode == RenderMode.PAGINATED && paginator != null && !showBars && paginatedPagerState.pageCount > 0,
enter = fadeIn(), enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(), exit = fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.BottomCenter) modifier = Modifier.align(Alignment.BottomCenter)
) { ) {
Box( Box(
@ -2922,8 +2922,8 @@ fun EpubReaderHost(
AnimatedVisibility( AnimatedVisibility(
visible = isAutoScrollControlsVisible, visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)),
modifier = Modifier modifier = Modifier
.align(BiasAlignment(alignmentBias, 1f)) .align(BiasAlignment(alignmentBias, 1f))
.padding(bottom = autoScrollPadding) .padding(bottom = autoScrollPadding)

View file

@ -53,29 +53,52 @@ class LocatorConverter(
try { try {
val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null
// 1. Parse CSS from the book val mergedByTag = mutableMapOf<String, MutableList<CssRule>>()
var parsingCssRules = OptimizedCssRules() val mergedByClass = mutableMapOf<String, MutableList<CssRule>>()
val mergedById = mutableMapOf<String, MutableList<CssRule>>()
val mergedOtherComplex = mutableListOf<CssRule>()
val density = Density(context) val density = Density(context)
val displayMetrics = context.resources.displayMetrics val displayMetrics = context.resources.displayMetrics
val constraints = Constraints(maxWidth = displayMetrics.widthPixels, maxHeight = displayMetrics.heightPixels) val constraints = Constraints(maxWidth = displayMetrics.widthPixels, maxHeight = displayMetrics.heightPixels)
fun aggregateRules(
target: MutableMap<String, MutableList<CssRule>>,
source: Map<String, List<CssRule>>
) {
source.forEach { (k, v) ->
target.getOrPut(k) { mutableListOf() }.addAll(v)
}
}
book.css.forEach { (path, content) -> book.css.forEach { (path, content) ->
val bookCssResult = CssParser.parse( val bookCssResult = CssParser.parse(
cssContent = content, cssContent = content,
cssPath = path, cssPath = path,
baseFontSizeSp = 16f, // A reasonable default for non-rendering parsing baseFontSizeSp = 16f,
density = density.density, density = density.density,
constraints = constraints, constraints = constraints,
isDarkTheme = false isDarkTheme = false
) )
parsingCssRules = parsingCssRules.merge(bookCssResult.rules)
val rules = bookCssResult.rules
aggregateRules(mergedByTag, rules.byTag)
aggregateRules(mergedByClass, rules.byClass)
aggregateRules(mergedById, rules.byId)
mergedOtherComplex.addAll(rules.otherComplex)
} }
// 2. Parse HTML to SemanticBlocks val parsingCssRules = OptimizedCssRules(
byTag = mergedByTag,
byClass = mergedByClass,
byId = mergedById,
otherComplex = mergedOtherComplex
)
val semanticBlocks = htmlToSemanticBlocks( val semanticBlocks = htmlToSemanticBlocks(
html = chapter.htmlContent, html = chapter.htmlContent,
cssRules = parsingCssRules, cssRules = parsingCssRules,
textStyle = TextStyle(), // Not used for rendering, so a default is fine textStyle = TextStyle(),
chapterAbsPath = chapter.absPath, chapterAbsPath = chapter.absPath,
extractionBasePath = book.extractionBasePath, extractionBasePath = book.extractionBasePath,
density = density, density = density,
@ -83,13 +106,12 @@ class LocatorConverter(
constraints = constraints constraints = constraints
) )
// 3. Serialize and cache the result
val protoBytes = proto.encodeToByteArray(semanticBlocks) val protoBytes = proto.encodeToByteArray(semanticBlocks)
val newCacheEntry = ProcessedChapter( val newCacheEntry = ProcessedChapter(
bookId = book.title, bookId = book.title,
chapterIndex = chapterIndex, chapterIndex = chapterIndex,
contentBlocksProto = protoBytes, contentBlocksProto = protoBytes,
estimatedPageCount = 0 // Page count is not relevant for locator conversion estimatedPageCount = 0
) )
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry)) bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
Timber.i("On-demand processing SUCCESS for chapter $chapterIndex.") Timber.i("On-demand processing SUCCESS for chapter $chapterIndex.")

View file

@ -354,21 +354,31 @@ data class OptimizedCssRules(
@ProtoNumber(4) val otherComplex: List<CssRule> = emptyList() @ProtoNumber(4) val otherComplex: List<CssRule> = emptyList()
) { ) {
fun merge(other: OptimizedCssRules): OptimizedCssRules { fun merge(other: OptimizedCssRules): OptimizedCssRules {
val mergedByTag = (this.byTag.asSequence() + other.byTag.asSequence()) fun mergeMap(
.groupBy({ it.key }, { it.value }) m1: Map<String, List<CssRule>>,
.mapValues { (_, values) -> values.flatten() } m2: Map<String, List<CssRule>>
): Map<String, List<CssRule>> {
if (m1.isEmpty()) return m2
if (m2.isEmpty()) return m1
val mergedByClass = (this.byClass.asSequence() + other.byClass.asSequence()) val result = LinkedHashMap(m1)
.groupBy({ it.key }, { it.value }) for ((key, value) in m2) {
.mapValues { (_, values) -> values.flatten() } val existing = result[key]
if (existing != null) {
result[key] = existing + value
} else {
result[key] = value
}
}
return result
}
val mergedById = (this.byId.asSequence() + other.byId.asSequence()) return OptimizedCssRules(
.groupBy({ it.key }, { it.value }) byTag = mergeMap(this.byTag, other.byTag),
.mapValues { (_, values) -> values.flatten() } byClass = mergeMap(this.byClass, other.byClass),
byId = mergeMap(this.byId, other.byId),
val mergedOtherComplex = this.otherComplex + other.otherComplex otherComplex = this.otherComplex + other.otherComplex
)
return OptimizedCssRules(mergedByTag, mergedByClass, mergedById, mergedOtherComplex)
} }
fun toFlatList(): List<CssRule> { fun toFlatList(): List<CssRule> {

View file

@ -45,6 +45,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
@ -4144,9 +4145,10 @@ fun PdfViewerScreen(
// Custom Top Bar // Custom Top Bar
AnimatedVisibility( AnimatedVisibility(
visible = showStandardBars, visible = showStandardBars,
enter = slideInVertically { fullHeight -> -fullHeight } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> -fullHeight } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { fullHeight -> -fullHeight } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> -fullHeight } + fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.TopCenter)) { modifier = Modifier.align(Alignment.TopCenter)
) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -4469,8 +4471,8 @@ fun PdfViewerScreen(
AnimatedVisibility( AnimatedVisibility(
visible = showStandardBars && isReflowingThisBook, visible = showStandardBars && isReflowingThisBook,
enter = fadeIn() + slideInVertically(), enter = fadeIn(animationSpec = tween(200)) + slideInVertically(animationSpec = tween(200)),
exit = fadeOut() + slideOutVertically(), exit = fadeOut(animationSpec = tween(200)) + slideOutVertically(animationSpec = tween(200)),
modifier = Modifier modifier = Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.padding(top = 56.dp) .padding(top = 56.dp)
@ -4579,11 +4581,12 @@ fun PdfViewerScreen(
// Search Navigation Controls // Search Navigation Controls
AnimatedVisibility( AnimatedVisibility(
visible = searchState.isSearchActive && !searchState.showSearchResultsPanel && smartSearchResult != null, visible = searchState.isSearchActive && !searchState.showSearchResultsPanel && smartSearchResult != null,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)),
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(bottom = 24.dp)) { .padding(bottom = 24.dp)
) {
val currentResult = currentPdfSearchResult val currentResult = currentPdfSearchResult
val searchData = smartSearchResult val searchData = smartSearchResult
@ -4691,9 +4694,10 @@ fun PdfViewerScreen(
// Bottom Bar // Bottom Bar
AnimatedVisibility( AnimatedVisibility(
visible = showStandardBars && !searchState.isSearchActive, visible = showStandardBars && !searchState.isSearchActive,
enter = slideInVertically { fullHeight -> fullHeight } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { fullHeight -> fullHeight } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.BottomCenter)) { modifier = Modifier.align(Alignment.BottomCenter)
) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -5925,8 +5929,8 @@ fun PdfViewerScreen(
AnimatedVisibility( AnimatedVisibility(
visible = isAutoScrollControlsVisible, visible = isAutoScrollControlsVisible,
enter = slideInVertically { it } + fadeIn(), enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically { it } + fadeOut(), exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)),
modifier = Modifier modifier = Modifier
.align(BiasAlignment(alignmentBias, 1f)) .align(BiasAlignment(alignmentBias, 1f))
.padding(bottom = autoScrollPadding) .padding(bottom = autoScrollPadding)