Refactored the TOC button functionality to handle cases where custom apps are configured to disable it.

* Fixed: Missing bottom margin in custom apps.
* Fixed: Reader's bottom app bar not appearing after closing or selecting a tab.
* Fixed: Menu not showing in the toolbar when the application is freshly launched in custom apps.
* Refactored the scroll behavior of the toolbar and bottom app bar to sync with WebView scrolling in the Compose UI.
This commit is contained in:
MohitMaliFtechiz 2025-06-24 01:23:18 +05:30
parent 2608694442
commit 3ebb37d5cc
22 changed files with 235 additions and 187 deletions

View File

@ -95,10 +95,10 @@ fun LocalLibraryScreen(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
KiwixAppBar(
stringResource(R.string.library),
navigationIcon,
state.actionMenuItems,
scrollBehavior
title = stringResource(R.string.library),
navigationIcon = navigationIcon,
actionMenuItems = state.actionMenuItems,
topAppBarScrollBehavior = scrollBehavior
)
},
floatingActionButton = { SelectFileButton(fabButtonClick) },

View File

@ -100,10 +100,10 @@ fun OnlineLibraryScreen(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
KiwixAppBar(
stringResource(string.download),
navigationIcon,
actionMenuItems,
scrollBehavior,
title = stringResource(string.download),
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems,
topAppBarScrollBehavior = scrollBehavior,
searchBar = searchBarIfActive(state)
)
},

View File

@ -211,12 +211,12 @@ class KiwixReaderFragment : CoreReaderFragment() {
}
}
override fun onPause() {
super.onPause()
// ScrollingViewWithBottomNavigationBehavior changes the margin to the size of the nav bar,
// this resets the margin to zero, before fragment navigation.
setBottomMarginToNavHostContainer(0)
}
// override fun onPause() {
// super.onPause()
// // ScrollingViewWithBottomNavigationBehavior changes the margin to the size of the nav bar,
// // this resets the margin to zero, before fragment navigation.
// setBottomMarginToNavHostContainer(ZERO)
// }
@Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {
@ -233,7 +233,6 @@ class KiwixReaderFragment : CoreReaderFragment() {
override fun onResume() {
super.onResume()
setFragmentContainerBottomMarginToSizeOfNavBar()
if (isFullScreenVideo || isInFullScreenMode()) {
hideNavBar()
}
@ -306,11 +305,10 @@ class KiwixReaderFragment : CoreReaderFragment() {
requireContext(),
this,
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"),
null,
requireNotNull(readerScreenState.value.fullScreenItem.second),
CoreWebViewClient(this, requireNotNull(zimReaderContainer)),
// requireNotNull(toolbarContainer),
// requireNotNull(bottomToolbar),
onToolbarOffsetChanged = { offsetY -> toolbarOffsetY.value = offsetY },
onBottomAppBarOffsetChanged = { bottomOffsetY -> bottomAppBarOffsetY.value = bottomOffsetY },
sharedPreferenceUtil = requireNotNull(sharedPreferenceUtil),
parentNavigationBar = requireActivity().findViewById(R.id.bottom_nav_view)
)

View File

@ -87,11 +87,20 @@ fun ZimHostScreen(
) {
KiwixTheme {
Scaffold(topBar = {
KiwixAppBar(stringResource(R.string.menu_wifi_hotspot), navigationIcon)
KiwixAppBar(
title = stringResource(R.string.menu_wifi_hotspot),
navigationIcon = navigationIcon
)
}) { contentPadding ->
Column(modifier = Modifier.fillMaxSize().padding(contentPadding)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = SIXTEEN_DP),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SIXTEEN_DP),
verticalAlignment = Alignment.CenterVertically
) {
ServerIpText(serverIpText, Modifier.weight(1f), LocalContext.current)

View File

@ -23,8 +23,7 @@
<fragment
android:id="@+id/readerFragment"
android:name="org.kiwix.kiwixmobile.nav.destination.reader.KiwixReaderFragment"
android:label="Reader"
tools:layout="@layout/fragment_reader">
android:label="Reader">
<argument
android:name="zimFileUri"
android:defaultValue=""

View File

@ -13,7 +13,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup?, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, videoView: ViewGroup?, webViewClient: CoreWebViewClient, private val onToolbarOffsetChanged: ((Float) -> Unit)? = null, private val onBottomAppBarOffsetChanged: ((Float) -> Unit)? = null, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>

View File

@ -1,30 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.extensions
import android.view.View
import android.widget.TextView
fun TextView.setTextAndVisibility(nullableText: String?) =
if (nullableText?.isNotEmpty() == true) {
text = nullableText
visibility = View.VISIBLE
} else {
visibility = View.GONE
}

View File

@ -73,7 +73,7 @@ fun HelpScreen(
KiwixTheme {
Scaffold(
topBar = {
KiwixAppBar(stringResource(R.string.menu_help), navigationIcon)
KiwixAppBar(title = stringResource(R.string.menu_help), navigationIcon = navigationIcon)
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {

View File

@ -72,7 +72,13 @@ fun AddNoteDialogScreen(
KiwixDialogTheme {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = snackBarHostState) },
topBar = { KiwixAppBar(stringResource(R.string.note), navigationIcon, actionMenuItems) }
topBar = {
KiwixAppBar(
title = stringResource(R.string.note),
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems
)
}
) { paddingValues ->
Column(
modifier = Modifier

View File

@ -23,10 +23,9 @@ import org.kiwix.videowebview.VideoEnabledWebChromeClient
class KiwixWebChromeClient(
private val callback: WebViewCallback,
nonVideoView: ViewGroup?,
videoView: ViewGroup?,
webView: KiwixWebView?
) : VideoEnabledWebChromeClient(nonVideoView, videoView, null, webView) {
) : VideoEnabledWebChromeClient(videoView, null, webView) {
override fun onProgressChanged(view: WebView, progress: Int) {
callback.webViewProgressChanged(progress, view)
}

View File

@ -59,7 +59,6 @@ open class KiwixWebView @SuppressLint("SetJavaScriptEnabled") constructor(
context: Context,
private val callback: WebViewCallback,
attrs: AttributeSet,
private var nonVideoView: ViewGroup?,
videoView: ViewGroup?,
private val webViewClient: CoreWebViewClient,
val sharedPreferenceUtil: SharedPreferenceUtil
@ -102,7 +101,7 @@ open class KiwixWebView @SuppressLint("SetJavaScriptEnabled") constructor(
clearCache(true)
setWebViewClient(webViewClient)
webChromeClient =
KiwixWebChromeClient(callback, nonVideoView, videoView, this).apply {
KiwixWebChromeClient(callback, videoView, this).apply {
setOnToggledFullscreen(
object : ToggledFullscreenCallback {
override fun toggledFullscreen(fullscreen: Boolean) {
@ -154,7 +153,6 @@ open class KiwixWebView @SuppressLint("SetJavaScriptEnabled") constructor(
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
nonVideoView = null
textZoomJob?.cancel()
textZoomJob = null
}

View File

@ -23,8 +23,12 @@ import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.COMPOSE_BOTTOM_APP_BAR_DEFAULT_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.COMPOSE_TOOLBAR_DEFAULT_HEIGHT
import org.kiwix.kiwixmobile.core.utils.DimenUtils.dpToPx
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import kotlin.math.max
import kotlin.math.min
@SuppressLint("ViewConstructor")
@Suppress("UnusedPrivateProperty")
@ -32,28 +36,72 @@ class ToolbarScrollingKiwixWebView @JvmOverloads constructor(
context: Context,
callback: WebViewCallback,
attrs: AttributeSet,
nonVideoView: ViewGroup?,
videoView: ViewGroup?,
webViewClient: CoreWebViewClient,
private val onToolbarOffsetChanged: ((Float) -> Unit)? = null,
private val onBottomAppBarOffsetChanged: ((Float) -> Unit)? = null,
sharedPreferenceUtil: SharedPreferenceUtil,
private val parentNavigationBar: View? = null
) : KiwixWebView(
context,
callback,
attrs,
nonVideoView,
videoView,
webViewClient,
sharedPreferenceUtil
) {
private val toolbarHeight = context.getToolbarHeight()
private val toolbarHeight = context.dpToPx(COMPOSE_TOOLBAR_DEFAULT_HEIGHT)
private val bottomAppBarHeightPx = context.dpToPx(COMPOSE_BOTTOM_APP_BAR_DEFAULT_HEIGHT)
private var startY = 0f
private var currentOffset = 0f
init {
fixInitalScrollingIssue()
}
/**
* Adjusts the internal offset of the WebView based on scroll delta.
*
* Positive scrollDelta = user scrolling down (hide UI)
* Negative scrollDelta = user scrolling up (show UI)
*/
private fun moveToolbar(scrollDelta: Int): Boolean {
val newOffset = when {
scrollDelta > 0 -> max(-toolbarHeight.toFloat(), currentOffset - scrollDelta)
else -> min(0f, currentOffset - scrollDelta)
}
if (newOffset != currentOffset) {
currentOffset = newOffset
notifyOffsetChanged(newOffset)
return true
}
return false
}
/**
* Notifies Compose UI about toolbar offset.
*/
private fun notifyOffsetChanged(offset: Float) {
onToolbarOffsetChanged?.invoke(offset)
// Compute offset for bottomAppBar using height ratio
val bottomOffset = offset * -1 * (bottomAppBarHeightPx.toFloat() / toolbarHeight)
onBottomAppBarOffsetChanged?.invoke(bottomOffset)
// Optional: Animate parent navigation bar (if still using it)
parentNavigationBar?.let { view ->
val offsetFactor = view.height / toolbarHeight.toFloat()
view.translationY = offset * -1 * offsetFactor
}
// Adjust WebView position to prevent layout jump
this.translationY = offset
}
/**
* The webview needs to be scrolled with 0 to not be slightly hidden on startup.
* See https://github.com/kiwix/kiwix-android/issues/2304 for issue description.
@ -62,68 +110,27 @@ class ToolbarScrollingKiwixWebView @JvmOverloads constructor(
moveToolbar(0)
}
@Suppress("FunctionOnlyReturningConstant", "UnusedParameter")
private fun moveToolbar(scrollDelta: Int): Boolean {
// val originalTranslation = toolbarView.translationY
// val newTranslation =
// if (scrollDelta > 0) {
// // scroll down
// max(-toolbarHeight.toFloat(), originalTranslation - scrollDelta)
// } else {
// // scroll up
// min(0f, originalTranslation - scrollDelta)
// }
//
// toolbarView.translationY = newTranslation
// bottomBarView.translationY =
// newTranslation * -1 * (bottomBarView.height / toolbarHeight.toFloat())
// parentNavigationBar?.let {
// it.translationY = newTranslation * -1 * (it.height / toolbarHeight.toFloat())
// }
// this.translationY = newTranslation + toolbarHeight
// return toolbarHeight + newTranslation != 0f && newTranslation != 0f
return false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
// val transY = toolbarView.translationY.toInt()
// when (event.actionMasked) {
// MotionEvent.ACTION_DOWN -> startY = event.rawY
// MotionEvent.ACTION_MOVE -> {
// // If we are in fullscreen don't scroll bar
// if (sharedPreferenceUtil.prefFullScreen) {
// return super.onTouchEvent(event)
// }
// // Filter out zooms since we don't want to affect the toolbar when zooming
// if (event.pointerCount == 1) {
// val diffY = (event.rawY - startY).toInt()
// startY = event.rawY
// if (moveToolbar(-diffY)) {
// event.offsetLocation(0f, -diffY.toFloat())
// return super.onTouchEvent(event)
// }
// }
// }
// // If the toolbar is half-visible,
// // either open or close it entirely depending on how far it is visible
// MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
// if (transY != 0 && transY > -toolbarHeight) {
// if (transY > -toolbarHeight / 2) {
// ensureToolbarDisplayed()
// } else {
// ensureToolbarHidden()
// }
// }
// }
if (sharedPreferenceUtil.prefFullScreen) return super.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
startY = event.rawY
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1) {
val diffY = (event.rawY - startY).toInt()
startY = event.rawY
if (moveToolbar(-diffY)) {
event.offsetLocation(0f, -diffY.toFloat())
return super.onTouchEvent(event)
}
}
}
}
return super.onTouchEvent(event)
}
private fun ensureToolbarDisplayed() {
moveToolbar(-toolbarHeight)
}
private fun ensureToolbarHidden() {
moveToolbar(toolbarHeight)
}
}

View File

@ -62,7 +62,6 @@ import androidx.annotation.AnimRes
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.SnackbarDuration
@ -171,7 +170,6 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchItemToOpen
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.components.rememberBottomNavigationVisibility
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getWindowWidth
@ -320,6 +318,8 @@ abstract class CoreReaderFragment :
private var isReadSelection = false
private var isReadAloudServiceRunning = false
private var libkiwixBook: Book? = null
val toolbarOffsetY = mutableStateOf(0f)
val bottomAppBarOffsetY = mutableStateOf(0f)
protected var readerMenuState: ReaderMenuState? = null
private var composeView: ComposeView? = null
@ -348,7 +348,7 @@ abstract class CoreReaderFragment :
previousPageButtonItem = Triple({ goBack() }, { showBackwardHistory() }, false),
onHomeButtonClick = { openMainPage() },
nextPageButtonItem = Triple({ goForward() }, { showForwardHistory() }, false),
onTocClick = { openToc() },
tocButtonItem = false to { },
onCloseAllTabs = { closeAllTabs() },
bottomNavigationHeight = ZERO,
shouldShowBottomAppBar = true,
@ -473,11 +473,6 @@ abstract class CoreReaderFragment :
readerMenuState = createMainMenu()
composeView?.apply {
setContent {
val lazyListState = rememberLazyListState()
val isBottomNavVisible = rememberBottomNavigationVisibility(lazyListState)
LaunchedEffect(isBottomNavVisible) {
(activity as? CoreMainActivity)?.toggleBottomNavigation(isBottomNavVisible)
}
LaunchedEffect(Unit) {
snapshotFlow { webViewList.size }
.distinctUntilChanged()
@ -491,7 +486,8 @@ abstract class CoreReaderFragment :
bottomNavigationHeight = getBottomNavigationHeight(),
readerScreenTitle = context.getString(R.string.reader),
darkModeViewPainter = darkModeViewPainter,
fullScreenItem = fullScreenItem.first to getVideoView()
fullScreenItem = fullScreenItem.first to getVideoView(),
tocButtonItem = getTocButtonStateAndAction()
)
}
}
@ -514,7 +510,8 @@ abstract class CoreReaderFragment :
iconTint = navigationIconTint()
)
},
listState = lazyListState
toolbarOffsetY = toolbarOffsetY,
bottomAppBarOffsetY = bottomAppBarOffsetY
)
DialogHost(alertDialogShower as AlertDialogShower)
}
@ -629,6 +626,20 @@ 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.
*
* @return A [Pair] containing:
* - [Boolean]: Indicates whether the TOC button should be enabled (e.g., can be disabled
* in certain custom app configurations where the sidebar is turned off).
* - [() -> Unit]: The action to perform when the TOC button is clicked.
*
* Note: If modifying this method, ensure it is thoroughly tested in custom app variants
* where sidebar behavior may differ.
*/
open fun getTocButtonStateAndAction(): Pair<Boolean, () -> Unit> = true to { openToc() }
private fun navigationIconContentDescription() =
if (readerMenuState?.isInTabSwitcher == true) {
R.string.search_open_in_new_tab
@ -949,7 +960,6 @@ abstract class CoreReaderFragment :
protected open fun hideTabSwitcher(shouldCloseZimBook: Boolean = true) {
setUpDrawerToggle()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
selectTab(currentWebViewIndex)
readerScreenState.update {
copy(
shouldShowBottomAppBar = true,
@ -958,6 +968,7 @@ abstract class CoreReaderFragment :
}
showSearchPlaceHolderInToolbar(false)
readerMenuState?.showWebViewOptions(urlIsValid())
selectTab(currentWebViewIndex)
// Reset the top margin of web views to 0 to remove any previously set margin
// This ensures that the web views are displayed without any additional top margin for kiwix custom apps.
// setTopMarginToWebViews(0)
@ -1077,7 +1088,7 @@ abstract class CoreReaderFragment :
}
}
private fun openToc() {
protected fun openToc() {
drawerLayout?.openDrawer(GravityCompat.END)
}
@ -1413,14 +1424,13 @@ abstract class CoreReaderFragment :
@Throws(IllegalArgumentException::class)
protected open fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? {
return ToolbarScrollingKiwixWebView(
requireActivity(),
requireContext(),
this,
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"),
null,
requireNotNull(readerScreenState.value.fullScreenItem.second),
CoreWebViewClient(this, requireNotNull(zimReaderContainer)),
// requireNotNull(toolbarContainer),
// requireNotNull(bottomToolbar),
onToolbarOffsetChanged = { offsetY -> toolbarOffsetY.value = offsetY },
onBottomAppBarOffsetChanged = { bottomOffsetY -> bottomAppBarOffsetY.value = bottomOffsetY },
requireNotNull(sharedPreferenceUtil)
)
}
@ -1821,7 +1831,6 @@ abstract class CoreReaderFragment :
// Show content if there is `Open Library` button showing
// and we are opening the ZIM file
hideNoBookOpenViews()
contentFrame?.visibility = VISIBLE
openAndSetInContainer(zimReaderSource)
updateTitle()
} else {
@ -2203,7 +2212,7 @@ abstract class CoreReaderFragment :
private fun updateBottomToolbarVisibility() {
readerScreenState.update {
copy(shouldShowBottomAppBar = !showTabSwitcher && !isInFullScreenMode())
copy(shouldShowBottomAppBar = readerMenuState?.isInTabSwitcher == false && !isInFullScreenMode())
}
}
@ -2882,6 +2891,7 @@ abstract class CoreReaderFragment :
}
selectTab(currentTab)
onComplete.invoke()
readerMenuState?.showWebViewOptions(urlIsValid())
} catch (ignore: Exception) {
Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", ignore)
activity.toast(R.string.could_not_restore_tabs, Toast.LENGTH_LONG)

View File

@ -61,7 +61,7 @@ const val TAB_MENU_ITEM_TESTING_TAG = "tabMenuItemTestingTag"
@Stable
class ReaderMenuState(
private val menuClickListener: MenuClickListener,
private val isUrlValidInitially: Boolean,
isUrlValidInitially: Boolean,
private val disableReadAloud: Boolean = false,
private val disableTabs: Boolean = false,
private val disableSearch: Boolean = false

View File

@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
@ -36,9 +37,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
@ -94,6 +97,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.delay
@ -144,12 +148,12 @@ const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTe
@Composable
fun ReaderScreen(
state: ReaderScreenState,
listState: LazyListState,
actionMenuItems: List<ActionMenuItem>,
toolbarOffsetY: MutableState<Float>,
bottomAppBarOffsetY: MutableState<Float>,
navigationIcon: @Composable () -> Unit
) {
val (bottomNavHeight, lazyListState) =
rememberScrollBehavior(state.bottomNavigationHeight, listState)
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
KiwixDialogTheme {
Scaffold(
@ -158,6 +162,7 @@ fun ReaderScreen(
ReaderTopBar(
state,
actionMenuItems,
toolbarOffsetY,
scrollBehavior,
navigationIcon
)
@ -166,9 +171,13 @@ fun ReaderScreen(
modifier = Modifier
.systemBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(bottom = bottomNavHeight.value)
.padding(bottom = bottomNavHeightInDp)
) { paddingValues ->
ReaderContentLayout(state, Modifier.padding(paddingValues))
ReaderContentLayout(
state,
Modifier.padding(paddingValues),
bottomAppBarOffsetY
)
}
}
}
@ -179,23 +188,33 @@ fun ReaderScreen(
private fun ReaderTopBar(
state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>,
toolbarOffsetY: MutableState<Float>,
scrollBehavior: TopAppBarScrollBehavior,
navigationIcon: @Composable () -> Unit,
) {
if (!state.shouldShowFullScreenMode && !state.fullScreenItem.first) {
val animatedOffsetY by animateDpAsState(
targetValue = with(LocalDensity.current) { toolbarOffsetY.value.toDp() },
label = "ToolbarScrollOffset"
)
KiwixAppBar(
title = if (state.showTabSwitcher) "" else state.readerScreenTitle,
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems,
topAppBarScrollBehavior = scrollBehavior,
searchBar =
searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps)
searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps),
modifier = Modifier.offset { IntOffset(x = ZERO, y = animatedOffsetY.roundToPx()) }
)
}
}
@Composable
private fun ReaderContentLayout(state: ReaderScreenState, modifier: Modifier = Modifier) {
private fun ReaderContentLayout(
state: ReaderScreenState,
modifier: Modifier = Modifier,
bottomAppBarOffsetY: MutableState<Float>
) {
Box(modifier = modifier.fillMaxSize()) {
when {
state.showTabSwitcher -> TabSwitcherView(
@ -210,7 +229,7 @@ private fun ReaderContentLayout(state: ReaderScreenState, modifier: Modifier = M
state.fullScreenItem.first -> ShowFullScreenView(state)
else -> {
ShowZIMFileContent(state)
ShowZIMFileContent(state, bottomAppBarOffsetY)
ShowProgressBarIfZIMFilePageIsLoading(state)
Column(Modifier.align(Alignment.BottomCenter)) {
TtsControls(state)
@ -219,8 +238,9 @@ private fun ReaderContentLayout(state: ReaderScreenState, modifier: Modifier = M
state.previousPageButtonItem,
state.onHomeButtonClick,
state.nextPageButtonItem,
state.onTocClick,
state.shouldShowBottomAppBar
state.tocButtonItem,
state.shouldShowBottomAppBar,
bottomAppBarOffsetY
)
}
CloseFullScreenImageButton(
@ -305,7 +325,15 @@ private fun BoxScope.CloseFullScreenImageButton(
}
@Composable
private fun ShowZIMFileContent(state: ReaderScreenState) {
private fun ShowZIMFileContent(
state: ReaderScreenState,
bottomAppBarOffsetY: MutableState<Float>
) {
val density = LocalDensity.current
val bottomNavHeightDp = with(density) { state.bottomNavigationHeight.toDp() }
val bottomAppBarOffsetDp = with(density) { -bottomAppBarOffsetY.value.toDp() }
val totalBottomPadding = (bottomNavHeightDp + bottomAppBarOffsetDp).coerceAtLeast(ZERO.dp)
state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) {
AndroidView(
@ -317,7 +345,10 @@ private fun ShowZIMFileContent(state: ReaderScreenState) {
addView(selectedWebView)
}
},
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(bottom = totalBottomPadding)
)
}
}
@ -425,14 +456,20 @@ private fun BottomAppBarOfReaderScreen(
previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onHomeButtonClick: () -> Unit,
nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onTocClick: () -> Unit,
shouldShowBottomAppBar: Boolean
tocButtonItem: Pair<Boolean, () -> Unit>,
shouldShowBottomAppBar: Boolean,
bottomAppBarOffsetY: MutableState<Float>
) {
if (!shouldShowBottomAppBar) return
val animatedOffsetY by animateDpAsState(
targetValue = with(LocalDensity.current) { bottomAppBarOffsetY.value.toDp() },
label = "BottomAppBarOffset"
)
BottomAppBar(
containerColor = Black,
contentColor = White,
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior(),
modifier = Modifier.offset { IntOffset(ZERO, animatedOffsetY.roundToPx()) }
) {
Row(
modifier = Modifier
@ -472,7 +509,8 @@ private fun BottomAppBarOfReaderScreen(
)
// Toggle Icon(to open the table of content in right side bar)
BottomAppBarButtonIcon(
onClick = onTocClick,
shouldEnable = tocButtonItem.first,
onClick = tocButtonItem.second,
buttonIcon = Drawable(R.drawable.ic_toc_24dp),
contentDescription = stringResource(R.string.table_of_contents)
)

View File

@ -132,8 +132,12 @@ data class ReaderScreenState(
val nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
/**
* Handles the click to open right sidebar button click in reader bottom toolbar.
*
* A [Pair] containing:
* - [Boolean]: Handles the button should enable or not(Specially for custom apps).
* - [Unit]: Handles the click of button.
*/
val onTocClick: () -> Unit,
val tocButtonItem: Pair<Boolean, () -> Unit>,
val onCloseAllTabs: () -> Unit,
/**
* Stores the height of the bottom navigation bar in pixels.

View File

@ -70,7 +70,13 @@ fun NavigationHistoryDialogScreen(
) {
KiwixDialogTheme {
Scaffold(
topBar = { KiwixAppBar(stringResource(titleId), navigationIcon, actionMenuItems) }
topBar = {
KiwixAppBar(
title = stringResource(titleId),
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems
)
}
) { paddingValues ->
Box(
modifier = Modifier

View File

@ -72,6 +72,7 @@ const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KiwixAppBar(
modifier: Modifier = Modifier,
title: String,
navigationIcon: @Composable () -> Unit,
actionMenuItems: List<ActionMenuItem> = emptyList(),
@ -92,7 +93,8 @@ fun KiwixAppBar(
// Edge-to-Edge mode is already enabled in our application,
// so we don't need to apply additional top insets.
// This prevents unwanted extra margin at the top.
windowInsets = WindowInsets.statusBars.only(WindowInsetsSides.Horizontal)
windowInsets = WindowInsets.statusBars.only(WindowInsetsSides.Horizontal),
modifier = modifier
)
}
}

View File

@ -193,4 +193,6 @@ object ComposeDimens {
val BACK_TO_TOP_BUTTON_BOTTOM_MARGIN = 80.dp
const val READER_BOTTOM_APP_BAR_DISABLE_BUTTON_ALPHA = 0.38f
val SEARCH_PLACEHOLDER_TEXT_SIZE = 12.sp
val COMPOSE_TOOLBAR_DEFAULT_HEIGHT = 64.dp
val COMPOSE_BOTTOM_APP_BAR_DEFAULT_HEIGHT = 80.dp
}

View File

@ -23,6 +23,7 @@ import android.os.Build
import android.util.DisplayMetrics
import android.util.TypedValue
import androidx.appcompat.R
import androidx.compose.ui.unit.Dp
object DimenUtils {
@JvmStatic fun Context.getToolbarHeight(): Int {
@ -33,6 +34,14 @@ object DimenUtils {
)
}
fun Context.dpToPx(dp: Dp): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.value,
resources.displayMetrics
).toInt()
}
@JvmStatic fun Activity.getWindowHeight(): Int =
computedDisplayMetric.heightPixels

View File

@ -55,7 +55,6 @@ open class VideoEnabledWebChromeClient :
fun toggledFullscreen(fullscreen: Boolean)
}
private var activityNonVideoView: View? = null
private var activityVideoView: ViewGroup? = null
private var loadingView: View? = null
private var webView: VideoEnabledWebView? = null
@ -84,14 +83,11 @@ open class VideoEnabledWebChromeClient :
/**
* Builds a video enabled WebChromeClient.
*
* @param activityNonVideoView A View in the activity's layout that contains every other view that
* should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video.
* Typically you would like this to fill the whole layout.
*/
@SuppressWarnings("unused")
constructor(activityNonVideoView: View?, activityVideoView: ViewGroup?) {
this.activityNonVideoView = activityNonVideoView
constructor(activityVideoView: ViewGroup?) {
this.activityVideoView = activityVideoView
loadingView = null
webView = null
@ -101,8 +97,6 @@ open class VideoEnabledWebChromeClient :
/**
* Builds a video enabled WebChromeClient.
*
* @param activityNonVideoView A View in the activity's layout that contains every other view that
* should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video.
* Typically you would like this to fill the whole layout.
* @param loadingView A View to be shown while the video is loading (typically only used in API
@ -110,11 +104,9 @@ open class VideoEnabledWebChromeClient :
*/
@SuppressWarnings("unused")
constructor(
activityNonVideoView: View?,
activityVideoView: ViewGroup?,
loadingView: View?
) {
this.activityNonVideoView = activityNonVideoView
this.activityVideoView = activityVideoView
this.loadingView = loadingView
webView = null
@ -124,8 +116,6 @@ open class VideoEnabledWebChromeClient :
/**
* Builds a video enabled WebChromeClient.
*
* @param activityNonVideoView A View in the activity's layout that contains every other view that
* should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video.
* Typically you would like this to fill the whole layout.
* @param loadingView A View to be shown while the video is loading (typically only used in API
@ -137,12 +127,10 @@ open class VideoEnabledWebChromeClient :
*/
@SuppressWarnings("unused")
constructor(
activityNonVideoView: View?,
activityVideoView: ViewGroup?,
loadingView: View?,
webView: VideoEnabledWebView?
) {
this.activityNonVideoView = activityNonVideoView
this.activityVideoView = activityVideoView
this.loadingView = loadingView
this.webView = webView
@ -177,9 +165,6 @@ open class VideoEnabledWebChromeClient :
isVideoFullscreen = true
videoViewContainer = view
videoViewCallback = callback
// Hide the non-video view, add the video view, and show it
activityNonVideoView?.visibility = View.INVISIBLE
activityVideoView?.addView(
videoViewContainer,
ViewGroup.LayoutParams(
@ -248,7 +233,6 @@ open class VideoEnabledWebChromeClient :
// Hide the video view, remove it, and show the non-video view
activityVideoView?.visibility = View.INVISIBLE
activityVideoView?.removeView(videoViewContainer)
activityNonVideoView?.visibility = View.VISIBLE
// Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
videoViewCallback?.let {

View File

@ -23,7 +23,6 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
@ -78,12 +77,6 @@ class CustomReaderFragment : CoreReaderFragment() {
if (isAdded) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
if (BuildConfig.DISABLE_SIDEBAR) {
val toolbarToc =
activity?.findViewById<ImageView>(org.kiwix.kiwixmobile.core.R.id.bottom_toolbar_toc)
toolbarToc?.isEnabled = false
// TODO refactor this with compose UI.
}
with(activity as AppCompatActivity) {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
setUpDrawerToggle()
@ -98,6 +91,20 @@ class CustomReaderFragment : CoreReaderFragment() {
}
}
/**
* Returns the TOC (Table of Contents) button's enabled state and click action.
*
* In this custom app variant, the TOC button is disabled if [BuildConfig.DISABLE_SIDEBAR] is `true`.
* This is typically used when the sidebar functionality is intentionally turned off.
*
* @return A [Pair] containing:
* - [Boolean]: `true` if the TOC button should be enabled (i.e., sidebar is allowed),
* `false` if it should be disabled (i.e., [DISABLE_SIDEBAR] is `true`).
* - [() -> Unit]: Action to execute when the button is clicked. This will only be invoked if enabled.
*/
override fun getTocButtonStateAndAction(): Pair<Boolean, () -> Unit> =
!BuildConfig.DISABLE_SIDEBAR to { openToc() }
/**
* Returns the tint color for the navigation icon.
*