Fixed: Clicking on a table of content item wasn’t scrolling the WebView to the selected section because the Compose ScrollState caused recomposition back to the previous scroll position — now replaced with a custom approach to smoothly scroll to the target section. * Fixed: The whole screen was scrolling when scrolling the table of contents.

* Fixed: App was crashing and not opening the intro screen on fresh install.
* Fixed: Design and appearance of the table of contents in dark mode.
This commit is contained in:
MohitMaliFtechiz 2025-07-29 02:35:53 +05:30
parent f041ee9cf0
commit 8afaa8be52
8 changed files with 150 additions and 127 deletions

View File

@ -33,6 +33,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@ -40,7 +41,6 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.compose.rememberNavController
import eu.mhutti1.utils.storage.StorageDevice
import eu.mhutti1.utils.storage.StorageDeviceUtils
@ -128,9 +128,17 @@ class KiwixMainActivity : CoreMainActivity() {
leftDrawerState = rememberDrawerState(DrawerValue.Closed)
uiCoroutineScope = rememberCoroutineScope()
bottomAppBarScrollBehaviour = BottomAppBarDefaults.exitAlwaysScrollBehavior()
val startDestination = remember {
if (sharedPreferenceUtil.showIntro() && !isIntroScreenNotVisible()) {
KiwixDestination.Intro.route
} else {
KiwixDestination.Reader.route
}
}
KiwixMainActivityScreen(
navController = navController,
leftDrawerContent = leftDrawerMenu,
startDestination = startDestination,
topLevelDestinationsRoute = topLevelDestinationsRoute,
leftDrawerState = leftDrawerState,
uiCoroutineScope = uiCoroutineScope,
@ -140,12 +148,6 @@ class KiwixMainActivity : CoreMainActivity() {
)
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange)
if (sharedPreferenceUtil.showIntro() && !isIntroScreenNotVisible()) {
val navOptions = NavOptions.Builder()
.setPopUpTo(KiwixDestination.Reader.route, inclusive = true)
.build()
navigate(KiwixDestination.Intro.route, navOptions)
}
}
DialogHost(alertDialogShower)
}

View File

@ -56,6 +56,7 @@ import org.kiwix.kiwixmobile.ui.KiwixNavGraph
fun KiwixMainActivityScreen(
navController: NavHostController,
leftDrawerContent: List<DrawerMenuGroup>,
startDestination: String,
topLevelDestinationsRoute: Set<String>,
leftDrawerState: DrawerState,
uiCoroutineScope: CoroutineScope,
@ -100,6 +101,7 @@ fun KiwixMainActivityScreen(
Box(modifier = Modifier.padding(paddingValues)) {
KiwixNavGraph(
navController = navController,
startDestination = startDestination,
modifier = Modifier.fillMaxSize()
)
}

View File

@ -221,9 +221,6 @@ class KiwixReaderFragment : CoreReaderFragment() {
exitBook()
}
override fun getBottomNavigationView(): BottomNavigationView? = null
// requireActivity().findViewById(R.id.bottom_nav_view)
/**
* Restores the view state based on the provided webViewHistoryItemList data and restore origin.
*

View File

@ -77,11 +77,12 @@ import org.kiwix.kiwixmobile.webserver.ZimHostFragment
@Composable
fun KiwixNavGraph(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = KiwixDestination.Reader.route,
startDestination = startDestination,
modifier = modifier
) {
composable(

View File

@ -73,7 +73,6 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -303,7 +302,6 @@ abstract class CoreReaderFragment :
nextPageButtonItem = Triple({ goForward() }, { showForwardHistory() }, false),
tocButtonItem = false to { },
onCloseAllTabs = { closeAllTabs() },
bottomNavigationHeight = ZERO,
shouldShowBottomAppBar = true,
selectedWebView = null,
readerScreenTitle = "",
@ -330,9 +328,7 @@ abstract class CoreReaderFragment :
appName = "",
donateButtonClick = {},
laterButtonClick = {},
tableOfContentTitle = "",
tableContentHeaderClick = { tableOfContentHeaderClick() },
tableOfContentSectionClick = { tableOfContentSectionClick(it) },
tableOfContentTitle = ""
)
)
private var readerLifeCycleScope: CoroutineScope? = null
@ -443,7 +439,6 @@ abstract class CoreReaderFragment :
LaunchedEffect(Unit) {
readerScreenState.update {
copy(
bottomNavigationHeight = getBottomNavigationHeight(),
readerScreenTitle = context.getString(string.reader),
darkModeViewPainter = darkModeViewPainter,
fullScreenItem = fullScreenItem.first to getVideoView(),
@ -482,7 +477,7 @@ abstract class CoreReaderFragment :
},
mainActivityBottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour,
documentSections = documentSections,
showTableOfContentDrawer = shouldTableOfContentDrawer,
showTableOfContentDrawer = shouldTableOfContentDrawer
)
DialogHost(alertDialogShower as AlertDialogShower)
}
@ -545,8 +540,6 @@ abstract class CoreReaderFragment :
}
}
private fun getBottomNavigationHeight(): Int = getBottomNavigationView()?.measuredHeight ?: ZERO
/**
* Provides the visibility state and click action for the TOC (Table of Contents) button
* shown in the reader's bottom app bar.
@ -677,31 +670,6 @@ abstract class CoreReaderFragment :
documentSections?.clear()
}
private fun tableOfContentHeaderClick() {
getCurrentWebView()?.scrollY = 0
shouldTableOfContentDrawer.update { false }
}
private fun tableOfContentSectionClick(position: Int) {
if (hasItemForPositionInDocumentSectionsList(position)) { // Bug Fix #3796
loadUrlWithCurrentWebview(
"javascript:document.getElementById('" +
documentSections?.get(position)?.id?.replace("'", "\\'") +
"').scrollIntoView();"
)
}
shouldTableOfContentDrawer.update { false }
}
private fun hasItemForPositionInDocumentSectionsList(position: Int): Boolean {
val documentListSize = documentSections?.size ?: return false
return when {
position < 0 -> false
position >= documentListSize -> false
else -> true
}
}
private fun showTabSwitcher() {
(requireActivity() as CoreMainActivity).apply {
disableDrawer()
@ -2699,8 +2667,6 @@ abstract class CoreReaderFragment :
* when handling invalid JSON scenarios.
*/
abstract fun restoreViewStateOnInvalidWebViewHistory()
abstract fun getBottomNavigationView(): BottomNavigationView?
}
enum class RestoreOrigin {

View File

@ -33,10 +33,12 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@ -86,12 +88,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@ -111,8 +115,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.update
import org.kiwix.kiwixmobile.core.main.DarkModeViewPainter
import org.kiwix.kiwixmobile.core.main.KiwixWebView
@ -130,6 +136,7 @@ import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue800
import org.kiwix.kiwixmobile.core.ui.theme.KiwixDialogTheme
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray700
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.BACK_TO_TOP_BUTTON_BOTTOM_MARGIN
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING
@ -139,6 +146,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_TOOLBAR_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.LARGE_BODY_TEXT_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE
@ -171,10 +179,12 @@ fun ReaderScreen(
mainActivityBottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?,
navigationIcon: @Composable () -> Unit
) {
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val localWebViewScrollState: MutableState<ScrollState?> =
remember { mutableStateOf(ScrollState(0)) }
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
KiwixDialogTheme {
Box(Modifier.fillMaxSize()) {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
@ -188,7 +198,6 @@ fun ReaderScreen(
floatingActionButton = { BackToTopFab(state) },
modifier = Modifier
.systemBarsPadding()
.padding(bottom = bottomNavHeightInDp)
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
.nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection)
.let { baseModifier ->
@ -198,26 +207,15 @@ fun ReaderScreen(
}
.semantics { testTag = READER_SCREEN_TESTING_TAG }
) { paddingValues ->
Box(Modifier.fillMaxSize()) {
ReaderContentLayout(
state,
Modifier.padding(paddingValues),
bottomAppBarScrollBehavior
)
AnimatedVisibility(
visible = showTableOfContentDrawer.value,
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
modifier = Modifier.align(Alignment.CenterEnd)
) {
TableDrawerSheet(
title = state.tableOfContentTitle,
sections = documentSections.orEmpty(),
onHeaderClick = state.tableContentHeaderClick,
onSectionClick = state.tableOfContentSectionClick
bottomAppBarScrollBehavior,
localWebViewScrollState.value
)
}
if (showTableOfContentDrawer.value) {
// Showing the background color on screen so that it look same as navigation drawer.
Box(
Modifier
.fillMaxSize()
@ -225,6 +223,21 @@ fun ReaderScreen(
.clickable { showTableOfContentDrawer.update { false } }
)
}
AnimatedVisibility(
visible = showTableOfContentDrawer.value,
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
modifier = Modifier
.systemBarsPadding()
.align(Alignment.CenterEnd)
) {
TableDrawerSheet(
title = state.tableOfContentTitle,
sections = documentSections.orEmpty(),
localWebViewScrollState,
state.selectedWebView,
showTableOfContentDrawer
)
}
}
}
@ -256,7 +269,8 @@ private fun ReaderTopBar(
private fun ReaderContentLayout(
state: ReaderScreenState,
modifier: Modifier = Modifier,
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior,
webViewScrollState: ScrollState?
) {
Box(modifier = modifier.fillMaxSize()) {
TabSwitcherAnimated(state)
@ -266,7 +280,7 @@ private fun ReaderContentLayout(
state.fullScreenItem.first -> ShowFullScreenView(state)
else -> {
ShowZIMFileContent(state)
ShowZIMFileContent(state, webViewScrollState)
ShowProgressBarIfZIMFilePageIsLoading(state)
Column(Modifier.align(Alignment.BottomCenter)) {
TtsControls(state)
@ -295,23 +309,40 @@ private fun ReaderContentLayout(
fun TableDrawerSheet(
title: String,
sections: List<DocumentSection>,
onHeaderClick: () -> Unit,
onSectionClick: (Int) -> Unit
webViewScrollState: MutableState<ScrollState?>,
selectedWebView: KiwixWebView?,
showTableOfContentDrawer: MutableState<Boolean>
) {
val drawerBackgroundColor = if (isSystemInDarkTheme()) {
MineShaftGray700
} else {
White
}
var scrollToSectionIndex by remember { mutableStateOf<Int?>(null) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(scrollToSectionIndex) {
scrollToSectionIndex?.let {
webViewScrollState.value = null
}
}
ModalDrawerSheet(
modifier = Modifier.width(NAVIGATION_DRAWER_WIDTH)
) {
LazyColumn(
modifier = Modifier
.fillMaxHeight()
modifier = Modifier.width(NAVIGATION_DRAWER_WIDTH),
drawerShape = RectangleShape,
drawerContainerColor = drawerBackgroundColor
) {
LazyColumn(modifier = Modifier.fillMaxHeight()) {
item {
Text(
text = title.ifEmpty { stringResource(id = R.string.no_section_info) },
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.fillMaxWidth()
.clickable { onHeaderClick() }
.clickable {
coroutineScope.launch {
webViewScrollState.value?.animateScrollTo(ZERO)
}
showTableOfContentDrawer.update { false }
}
.padding(horizontal = SIXTEEN_DP, vertical = TWELVE_DP)
)
}
@ -319,15 +350,46 @@ fun TableDrawerSheet(
val paddingStart = (section.level - ONE) * TWELVE
Text(
text = section.title,
style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Light,
fontSize = LARGE_BODY_TEXT_SIZE
),
modifier = Modifier
.fillMaxWidth()
.clickable { onSectionClick(index) }
.clickable { scrollToSectionIndex = index }
.padding(start = paddingStart.dp, top = EIGHT_DP, bottom = EIGHT_DP, end = SIXTEEN_DP)
)
}
}
}
LaunchedEffect(webViewScrollState.value) {
if (webViewScrollState.value == null &&
scrollToSectionIndex != null &&
hasItemForPositionInDocumentSectionsList(scrollToSectionIndex!!, sections)
) {
val targetId = sections[scrollToSectionIndex!!].id.replace("'", "\\'")
selectedWebView?.evaluateJavascript(
"document.getElementById('$targetId')?.scrollIntoView();",
null
)
delay(100)
webViewScrollState.value = ScrollState(selectedWebView?.scrollY ?: ZERO)
scrollToSectionIndex = null
showTableOfContentDrawer.update { false }
}
}
}
private fun hasItemForPositionInDocumentSectionsList(
position: Int,
sections: List<DocumentSection>
): Boolean {
val documentListSize = sections.size
return when {
position < 0 -> false
position >= documentListSize -> false
else -> true
}
}
@Composable
@ -433,12 +495,13 @@ private fun BoxScope.CloseFullScreenImageButton(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ShowZIMFileContent(state: ReaderScreenState) {
private fun ShowZIMFileContent(state: ReaderScreenState, webViewScrollState: ScrollState?) {
state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) {
ScrollableWebViewWithNestedScroll(
selectedWebView = selectedWebView,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
webViewScrollState = webViewScrollState
)
}
}
@ -448,12 +511,19 @@ private fun ShowZIMFileContent(state: ReaderScreenState) {
@Composable
fun ScrollableWebViewWithNestedScroll(
selectedWebView: KiwixWebView,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
webViewScrollState: ScrollState?
) {
Box(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.let { baseModifier ->
webViewScrollState?.let {
baseModifier.verticalScroll(it)
} ?: run {
baseModifier
}
}
) {
AndroidView(
factory = { context ->

View File

@ -139,10 +139,6 @@ data class ReaderScreenState(
*/
val tocButtonItem: Pair<Boolean, () -> Unit>,
val onCloseAllTabs: () -> Unit,
/**
* Stores the height of the bottom navigation bar in pixels.
*/
val bottomNavigationHeight: Int,
/**
* Manages the showing of Reader's [BottomAppBarOfReaderScreen].
*/
@ -172,13 +168,5 @@ data class ReaderScreenState(
/**
* Manages the showing of header title of "table of content".
*/
val tableOfContentTitle: String,
/**
* Handles the click when user clicks on the "Header" of "table of content".
*/
val tableContentHeaderClick: () -> Unit,
/**
* Handles the click when user clicks on the "section" of "table of content".
*/
val tableOfContentSectionClick: (Int) -> Unit
val tableOfContentTitle: String
)

View File

@ -173,9 +173,6 @@ class CustomReaderFragment : CoreReaderFragment() {
openHomeScreen()
}
// Since custom apps do not have the bottomNavigationView, so returning null.
override fun getBottomNavigationView(): BottomNavigationView? = null
/**
* Restores the view state when the webViewHistory data is valid.
* This method restores the tabs with webView pages history.