diff --git a/app/src/main/java/com/aryan/reader/HomeScreen.kt b/app/src/main/java/com/aryan/reader/HomeScreen.kt index a933be4..37230bd 100644 --- a/app/src/main/java/com/aryan/reader/HomeScreen.kt +++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt @@ -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 = { if (isContextualModeActive) { viewModel.clearContextualAction() } - pickFileLauncher.launch(arrayOf("*/*")) + try { + 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()) { diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt index 05ece93..0de526c 100644 --- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt +++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt @@ -109,6 +109,7 @@ import com.aryan.reader.data.RecentFileItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import timber.log.Timber import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -156,7 +157,14 @@ fun LibraryScreen( 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 -> 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 = { if (isContextualModeActive) { viewModel.clearContextualAction() } - pickFileLauncher.launch(arrayOf("*/*")) + try { + 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) { diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt index 1a09ccb..27a2b44 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt @@ -156,8 +156,8 @@ fun EpubReaderTopBar( ) { AnimatedVisibility( visible = isVisible, - enter = slideInVertically { -it } + fadeIn(), - exit = slideOutVertically { -it } + fadeOut(), + enter = slideInVertically(animationSpec = tween(200)) { -it } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { -it } + fadeOut(animationSpec = tween(200)), modifier = modifier ) { Surface( @@ -347,8 +347,8 @@ fun EpubReaderBottomBar( ) { AnimatedVisibility( visible = isVisible, - enter = slideInVertically { it } + fadeIn(), - exit = slideOutVertically { it } + fadeOut(), + enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)), modifier = modifier ) { Surface( @@ -456,11 +456,10 @@ fun EpubReaderPageSlider( ) { AnimatedVisibility( visible = isVisible, - enter = slideInVertically { fullHeight -> fullHeight } + fadeIn(), - exit = slideOutVertically { fullHeight -> fullHeight } + fadeOut() + enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)) ) { Box(modifier = Modifier.fillMaxSize()) { - // Dismiss area Box( modifier = Modifier .fillMaxSize() @@ -470,14 +469,6 @@ fun EpubReaderPageSlider( ) { 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( onClick = onClose, modifier = Modifier diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index 3a836c6..93f8d9a 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -2667,8 +2667,8 @@ fun EpubReaderHost( // Page Info Bar (Vertical) AnimatedVisibility( visible = renderMode == RenderMode.VERTICAL_SCROLL && !showBars, - enter = fadeIn(), - exit = fadeOut(), + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(200)), modifier = Modifier.align(Alignment.BottomCenter) ) { Box( @@ -2710,8 +2710,8 @@ fun EpubReaderHost( // Page Info Bar (Paginated) AnimatedVisibility( visible = renderMode == RenderMode.PAGINATED && paginator != null && !showBars && paginatedPagerState.pageCount > 0, - enter = fadeIn(), - exit = fadeOut(), + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(200)), modifier = Modifier.align(Alignment.BottomCenter) ) { Box( @@ -2922,8 +2922,8 @@ fun EpubReaderHost( AnimatedVisibility( visible = isAutoScrollControlsVisible, - enter = slideInVertically { it } + fadeIn(), - exit = slideOutVertically { it } + fadeOut(), + enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)), modifier = Modifier .align(BiasAlignment(alignmentBias, 1f)) .padding(bottom = autoScrollPadding) diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt index d53f07c..57f78d1 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt @@ -53,29 +53,52 @@ class LocatorConverter( try { val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null - // 1. Parse CSS from the book - var parsingCssRules = OptimizedCssRules() + val mergedByTag = mutableMapOf>() + val mergedByClass = mutableMapOf>() + val mergedById = mutableMapOf>() + val mergedOtherComplex = mutableListOf() + val density = Density(context) val displayMetrics = context.resources.displayMetrics val constraints = Constraints(maxWidth = displayMetrics.widthPixels, maxHeight = displayMetrics.heightPixels) + fun aggregateRules( + target: MutableMap>, + source: Map> + ) { + source.forEach { (k, v) -> + target.getOrPut(k) { mutableListOf() }.addAll(v) + } + } + book.css.forEach { (path, content) -> val bookCssResult = CssParser.parse( cssContent = content, cssPath = path, - baseFontSizeSp = 16f, // A reasonable default for non-rendering parsing + baseFontSizeSp = 16f, density = density.density, constraints = constraints, 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( html = chapter.htmlContent, cssRules = parsingCssRules, - textStyle = TextStyle(), // Not used for rendering, so a default is fine + textStyle = TextStyle(), chapterAbsPath = chapter.absPath, extractionBasePath = book.extractionBasePath, density = density, @@ -83,13 +106,12 @@ class LocatorConverter( constraints = constraints ) - // 3. Serialize and cache the result val protoBytes = proto.encodeToByteArray(semanticBlocks) val newCacheEntry = ProcessedChapter( bookId = book.title, chapterIndex = chapterIndex, contentBlocksProto = protoBytes, - estimatedPageCount = 0 // Page count is not relevant for locator conversion + estimatedPageCount = 0 ) bookCacheDao.insertProcessedChapters(listOf(newCacheEntry)) Timber.i("On-demand processing SUCCESS for chapter $chapterIndex.") diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt index b048119..7491f06 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderData.kt @@ -354,21 +354,31 @@ data class OptimizedCssRules( @ProtoNumber(4) val otherComplex: List = emptyList() ) { fun merge(other: OptimizedCssRules): OptimizedCssRules { - val mergedByTag = (this.byTag.asSequence() + other.byTag.asSequence()) - .groupBy({ it.key }, { it.value }) - .mapValues { (_, values) -> values.flatten() } + fun mergeMap( + m1: Map>, + m2: Map> + ): Map> { + if (m1.isEmpty()) return m2 + if (m2.isEmpty()) return m1 - val mergedByClass = (this.byClass.asSequence() + other.byClass.asSequence()) - .groupBy({ it.key }, { it.value }) - .mapValues { (_, values) -> values.flatten() } + val result = LinkedHashMap(m1) + for ((key, value) in m2) { + 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()) - .groupBy({ it.key }, { it.value }) - .mapValues { (_, values) -> values.flatten() } - - val mergedOtherComplex = this.otherComplex + other.otherComplex - - return OptimizedCssRules(mergedByTag, mergedByClass, mergedById, mergedOtherComplex) + return OptimizedCssRules( + byTag = mergeMap(this.byTag, other.byTag), + byClass = mergeMap(this.byClass, other.byClass), + byId = mergeMap(this.byId, other.byId), + otherComplex = this.otherComplex + other.otherComplex + ) } fun toFlatList(): List { diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index cfd87e3..e20f8be 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically @@ -4144,9 +4145,10 @@ fun PdfViewerScreen( // Custom Top Bar AnimatedVisibility( visible = showStandardBars, - enter = slideInVertically { fullHeight -> -fullHeight } + fadeIn(), - exit = slideOutVertically { fullHeight -> -fullHeight } + fadeOut(), - modifier = Modifier.align(Alignment.TopCenter)) { + enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> -fullHeight } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> -fullHeight } + fadeOut(animationSpec = tween(200)), + modifier = Modifier.align(Alignment.TopCenter) + ) { Surface( modifier = Modifier .fillMaxWidth() @@ -4469,8 +4471,8 @@ fun PdfViewerScreen( AnimatedVisibility( visible = showStandardBars && isReflowingThisBook, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), + enter = fadeIn(animationSpec = tween(200)) + slideInVertically(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(200)) + slideOutVertically(animationSpec = tween(200)), modifier = Modifier .align(Alignment.TopCenter) .padding(top = 56.dp) @@ -4579,11 +4581,12 @@ fun PdfViewerScreen( // Search Navigation Controls AnimatedVisibility( visible = searchState.isSearchActive && !searchState.showSearchResultsPanel && smartSearchResult != null, - enter = slideInVertically { it } + fadeIn(), - exit = slideOutVertically { it } + fadeOut(), + enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)), modifier = Modifier .align(Alignment.BottomCenter) - .padding(bottom = 24.dp)) { + .padding(bottom = 24.dp) + ) { val currentResult = currentPdfSearchResult val searchData = smartSearchResult @@ -4691,9 +4694,10 @@ fun PdfViewerScreen( // Bottom Bar AnimatedVisibility( visible = showStandardBars && !searchState.isSearchActive, - enter = slideInVertically { fullHeight -> fullHeight } + fadeIn(), - exit = slideOutVertically { fullHeight -> fullHeight } + fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter)) { + enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)), + modifier = Modifier.align(Alignment.BottomCenter) + ) { Surface( modifier = Modifier .fillMaxWidth() @@ -5925,8 +5929,8 @@ fun PdfViewerScreen( AnimatedVisibility( visible = isAutoScrollControlsVisible, - enter = slideInVertically { it } + fadeIn(), - exit = slideOutVertically { it } + fadeOut(), + enter = slideInVertically(animationSpec = tween(200)) { it } + fadeIn(animationSpec = tween(200)), + exit = slideOutVertically(animationSpec = tween(200)) { it } + fadeOut(animationSpec = tween(200)), modifier = Modifier .align(BiasAlignment(alignmentBias, 1f)) .padding(bottom = autoScrollPadding)