Fixed: WebView was not loading content properly in Compose.

* Refactored KiwixAppBar to support a string title instead of a string resource ID, allowing dynamic titles like ZIM file names.
* Fixed: ZIM file title was not appearing correctly in the toolbar.
* Refactored all snackbar-related functionalities on the reader screen.
* Updated KiwixAppBar to support an overflow menu.
* Introduced ReaderMenuState to manage menu item state dynamically for improved maintainability.
This commit is contained in:
MohitMaliFtechiz 2025-06-17 00:51:29 +05:30
parent 6ea560407d
commit 5c879484a6
27 changed files with 575 additions and 317 deletions

View File

@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.CollectSideEffectWithActivity
@ -70,7 +71,7 @@ fun LanguageScreen(
}
Scaffold(topBar = {
KiwixAppBar(
titleId = R.string.select_languages,
title = stringResource(R.string.select_languages),
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItemList,
searchBar = if (isSearchActive) {

View File

@ -113,7 +113,7 @@ fun LocalFileTransferScreen(
Scaffold(
topBar = {
KiwixAppBar(
titleId = toolbarTitle,
title = stringResource(toolbarTitle),
actionMenuItems = actionMenuItems.map {
it.copy(
modifier =

View File

@ -233,7 +233,7 @@ class KiwixMainActivity : CoreMainActivity() {
*
* TODO Remove this once we migrate to compose.
*/
fun toggleBottomNavigation(isVisible: Boolean) {
override fun toggleBottomNavigation(isVisible: Boolean) {
activityKiwixMainBinding.bottomNavView.animate()
?.translationY(
if (isVisible) {

View File

@ -38,32 +38,24 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.main.reader.CONTENT_LOADING_PROGRESSBAR_TESTING_TAG
import org.kiwix.kiwixmobile.core.main.reader.rememberScrollBehavior
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.components.ScrollDirection
import org.kiwix.kiwixmobile.core.ui.components.SwipeRefreshLayout
import org.kiwix.kiwixmobile.core.ui.components.rememberLazyListScrollListener
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White
@ -102,7 +94,12 @@ fun LocalLibraryScreen(
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems, scrollBehavior)
KiwixAppBar(
stringResource(R.string.library),
navigationIcon,
state.actionMenuItems,
scrollBehavior
)
},
floatingActionButton = { SelectFileButton(fabButtonClick) },
modifier = Modifier
@ -141,33 +138,6 @@ fun LocalLibraryScreen(
}
}
@Composable
fun rememberScrollBehavior(
bottomNavigationHeight: Int,
listState: LazyListState,
): Pair<MutableState<Dp>, LazyListState> {
val bottomNavHeightInDp = with(LocalDensity.current) { bottomNavigationHeight.toDp() }
val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) }
val lazyListState = rememberLazyListScrollListener(
lazyListState = listState,
onScrollChanged = { direction ->
when (direction) {
ScrollDirection.SCROLL_UP -> {
bottomNavHeight.value = bottomNavHeightInDp
}
ScrollDirection.SCROLL_DOWN -> {
bottomNavHeight.value = ZERO.dp
}
ScrollDirection.IDLE -> {}
}
}
)
return bottomNavHeight to lazyListState
}
@Composable
private fun BookItemList(
state: FileSelectListState,

View File

@ -53,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
import org.kiwix.kiwixmobile.core.main.reader.rememberScrollBehavior
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
@ -72,7 +73,6 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIX_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP
import org.kiwix.kiwixmobile.nav.destination.library.local.rememberScrollBehavior
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
@ -100,7 +100,7 @@ fun OnlineLibraryScreen(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
KiwixAppBar(
string.download,
stringResource(string.download),
navigationIcon,
actionMenuItems,
scrollBehavior,

View File

@ -27,11 +27,11 @@ import android.view.MenuInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.R
@ -51,12 +51,12 @@ import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.extensions.update
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.CoreWebViewClient
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromSearchScreen
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
@ -253,6 +253,9 @@ class KiwixReaderFragment : CoreReaderFragment() {
exitBook()
}
override fun getBottomNavigationView(): BottomNavigationView? =
requireActivity().findViewById(R.id.bottom_nav_view)
/**
* Restores the view state based on the provided webViewHistoryItemList data and restore origin.
*
@ -291,7 +294,10 @@ class KiwixReaderFragment : CoreReaderFragment() {
}
restoreTabs(webViewHistoryItemList, currentTab, onComplete)
} else {
getCurrentWebView()?.snack(string.zim_not_opened)
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(string.zim_not_opened),
lifecycleScope = lifecycleScope
)
exitBook() // hide the options for zim file to avoid unexpected UI behavior
}
}
@ -305,16 +311,17 @@ class KiwixReaderFragment : CoreReaderFragment() {
@Throws(IllegalArgumentException::class)
override fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? {
requireNotNull(activityMainRoot)
// requireNotNull(activityMainRoot)
return ToolbarScrollingKiwixWebView(
requireContext(),
this,
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"),
activityMainRoot as ViewGroup,
requireNotNull(videoView),
null,
// requireNotNull(videoView),
null,
CoreWebViewClient(this, requireNotNull(zimReaderContainer)),
requireNotNull(toolbarContainer),
requireNotNull(bottomToolbar),
// requireNotNull(toolbarContainer),
// requireNotNull(bottomToolbar),
sharedPreferenceUtil = requireNotNull(sharedPreferenceUtil),
parentNavigationBar = requireActivity().findViewById(R.id.bottom_nav_view)
)

View File

@ -87,7 +87,7 @@ fun ZimHostScreen(
) {
KiwixTheme {
Scaffold(topBar = {
KiwixAppBar(R.string.menu_wifi_hotspot, navigationIcon)
KiwixAppBar(stringResource(R.string.menu_wifi_hotspot), navigationIcon)
}) { contentPadding ->
Column(modifier = Modifier.fillMaxSize().padding(contentPadding)) {
Row(

View File

@ -30,7 +30,8 @@ fun SnackbarHostState.snack(
actionClick: (() -> Unit)? = null,
// Default duration is 4 seconds.
snackbarDuration: SnackbarDuration = SnackbarDuration.Short,
lifecycleScope: CoroutineScope
lifecycleScope: CoroutineScope,
snackBarResult: (SnackbarResult) -> Unit = {}
) {
lifecycleScope.launch {
val result = showSnackbar(
@ -41,5 +42,6 @@ fun SnackbarHostState.snack(
if (result == SnackbarResult.ActionPerformed) {
actionClick?.invoke()
}
snackBarResult.invoke(result)
}
}

View File

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

View File

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

View File

@ -495,4 +495,14 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
abstract val readerFragmentResId: Int
abstract fun createApplicationShortcuts()
abstract fun setDialogHostToActivity(alertDialogShower: AlertDialogShower)
/**
* This is for showing and hiding the bottomNavigationView when user scroll the screen.
* We are making this abstract so that it can be easily used from the reader screen.
* Since we do not have the bottomNavigationView in custom apps. So doing this way both apps will
* provide there own implementation.
*
* TODO we will remove this once we will migrate mainActivity to the compose.
*/
abstract fun toggleBottomNavigation(isVisible: Boolean)
}

View File

@ -60,7 +60,7 @@ open class KiwixWebView @SuppressLint("SetJavaScriptEnabled") constructor(
private val callback: WebViewCallback,
attrs: AttributeSet,
private var nonVideoView: ViewGroup?,
videoView: ViewGroup,
videoView: ViewGroup?,
private val webViewClient: CoreWebViewClient,
val sharedPreferenceUtil: SharedPreferenceUtil
) : VideoEnabledWebView(context, attrs) {

View File

@ -25,19 +25,16 @@ import android.view.View
import android.view.ViewGroup
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import kotlin.math.max
import kotlin.math.min
@SuppressLint("ViewConstructor")
@Suppress("UnusedPrivateProperty")
class ToolbarScrollingKiwixWebView @JvmOverloads constructor(
context: Context,
callback: WebViewCallback,
attrs: AttributeSet,
nonVideoView: ViewGroup,
videoView: ViewGroup,
nonVideoView: ViewGroup?,
videoView: ViewGroup?,
webViewClient: CoreWebViewClient,
private val toolbarView: View,
private val bottomBarView: View,
sharedPreferenceUtil: SharedPreferenceUtil,
private val parentNavigationBar: View? = null
) : KiwixWebView(
@ -65,58 +62,60 @@ 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
// 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()
}
}
}
// 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()
// }
// }
// }
return super.onTouchEvent(event)
}

View File

@ -44,8 +44,6 @@ import android.view.Gravity.BOTTOM
import android.view.Gravity.CENTER_HORIZONTAL
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.View.GONE
@ -67,27 +65,28 @@ 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
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.core.view.GravityCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
@ -96,9 +95,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -151,7 +150,6 @@ import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnInitSucceedListener
import org.kiwix.kiwixmobile.core.main.KiwixTextToSpeech.OnSpeakingListener
import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.MainMenu.MenuClickListener
import org.kiwix.kiwixmobile.core.main.MainRepositoryActions
import org.kiwix.kiwixmobile.core.main.OnSwipeTouchListener
import org.kiwix.kiwixmobile.core.main.ServiceWorkerUninitialiser
@ -181,6 +179,7 @@ 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.utils.AnimationUtils.rotate
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
@ -224,13 +223,13 @@ const val SEARCH_ITEM_TITLE_KEY = "searchItemTitle"
abstract class CoreReaderFragment :
BaseFragment(),
WebViewCallback,
MenuClickListener,
ReaderMenuState.MenuClickListener,
FragmentActivityExtensions,
WebViewProvider,
ReadAloudCallbacks,
NavigationHistoryClickListener,
ShowDonationDialogCallback {
protected val webViewList: MutableList<KiwixWebView> = ArrayList()
protected val webViewList = mutableStateListOf<KiwixWebView>()
private val webUrlsFlow = MutableStateFlow("")
private var fragmentReaderBinding: FragmentReaderBinding? = null
@ -317,8 +316,6 @@ abstract class CoreReaderFragment :
private var tabRecyclerView: RecyclerView? = null
private var snackBarRoot: CoordinatorLayout? = null
private var noOpenBookText: TextView? = null
private var bottomToolbarToc: ImageView? = null
@ -395,11 +392,15 @@ abstract class CoreReaderFragment :
{ goToBookmarks() },
IconItem.Drawable(R.drawable.ic_bookmark_border_24dp)
),
previousPageButtonItem = { goBack() } to { showBackwardHistory() },
previousPageButtonItem = Triple({ goBack() }, { showBackwardHistory() }, false),
onHomeButtonClick = { openMainPage() },
nextPageButtonItem = { goForward() } to { showForwardHistory() },
nextPageButtonItem = Triple({ goForward() }, { showForwardHistory() }, false),
onTocClick = { openToc() },
onCloseAllTabs = { closeAllTabs() }
onCloseAllTabs = { closeAllTabs() },
bottomNavigationHeight = ZERO,
shouldShowBottomAppBar = true,
selectedWebView = null,
readerScreenTitle = ""
)
)
private var readerLifeCycleScope: CoroutineScope? = null
@ -498,6 +499,19 @@ abstract class CoreReaderFragment :
super.onViewCreated(view, savedInstanceState)
composeView?.apply {
setContent {
val lazyListState = rememberLazyListState()
val isBottomNavVisible = rememberBottomNavigationVisibility(lazyListState)
LaunchedEffect(isBottomNavVisible) {
(requireActivity() as CoreMainActivity).toggleBottomNavigation(isBottomNavVisible)
}
LaunchedEffect(Unit) {
readerScreenState.update {
copy(
bottomNavigationHeight = getBottomNavigationHeight(),
readerScreenTitle = context.getString(R.string.reader)
)
}
}
ReaderScreen(
state = readerScreenState.value,
actionMenuItems = emptyList(),
@ -507,13 +521,13 @@ abstract class CoreReaderFragment :
contentDescription = string.open_drawer,
onClick = { navigationIconClick() }
)
}
},
listState = lazyListState
)
DialogHost(alertDialogShower as AlertDialogShower)
}
}
addAlertDialogToDialogHost()
setupMenu()
donationDialogHandler?.setDonationDialogCallBack(this)
val activity = requireActivity() as AppCompatActivity?
activity?.let {
@ -613,6 +627,8 @@ abstract class CoreReaderFragment :
handleClicks()
}
private fun getBottomNavigationHeight(): Int = getBottomNavigationView()?.measuredHeight ?: ZERO
private fun navigationIconClick() {
// Manually handle the navigation open/close.
// Since currently we are using the view based navigation drawer in other screens.
@ -657,7 +673,6 @@ abstract class CoreReaderFragment :
bottomToolbarArrowForward = findViewById(R.id.bottom_toolbar_arrow_forward)
bottomToolbarHome = findViewById(R.id.bottom_toolbar_home)
tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view)
snackBarRoot = findViewById(R.id.snackbar_root)
bottomToolbarToc = findViewById(R.id.bottom_toolbar_toc)
donationLayout = findViewById(R.id.donation_layout)
}
@ -912,7 +927,9 @@ abstract class CoreReaderFragment :
// the unwanted blank space caused by the toolbar.
setTopMarginToWebViews(-requireActivity().getToolbarHeight())
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
bottomToolbar?.visibility = GONE
readerScreenState.update {
copy(shouldShowBottomAppBar = false)
}
contentFrame?.visibility = GONE
progressBar?.visibility = GONE
backToTopButton?.hide()
@ -1123,19 +1140,12 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber")
private fun updateBottomToolbarArrowsAlpha() {
bottomToolbarArrowForward?.let {
if (getCurrentWebView()?.canGoForward() == true) {
bottomToolbarArrowForward?.alpha = 1f
} else {
bottomToolbarArrowForward?.alpha = 0.6f
}
}
bottomToolbarArrowBack?.let {
if (getCurrentWebView()?.canGoBack() == true) {
bottomToolbarArrowBack?.alpha = 1f
} else {
bottomToolbarArrowBack?.alpha = 0.6f
}
val currentWebView = getCurrentWebView()
readerScreenState.update {
copy(
previousPageButtonItem = previousPageButtonItem.copy(third = currentWebView?.canGoBack() == true),
nextPageButtonItem = nextPageButtonItem.copy(third = currentWebView?.canGoForward() == true)
)
}
}
@ -1224,7 +1234,9 @@ abstract class CoreReaderFragment :
*/
open fun updateTitle() {
if (isAdded) {
actionBar?.title = getValidTitle(zimReaderContainer?.zimFileTitle)
readerScreenState.update {
copy(readerScreenTitle = getValidTitle(zimReaderContainer?.zimFileTitle))
}
}
}
@ -1350,7 +1362,6 @@ abstract class CoreReaderFragment :
super.onDestroyView()
findInPageTitle = null
searchItemToOpen = null
restoreTabsSnackbarCallback = null
try {
coreReaderLifeCycleScope?.cancel()
readerLifeCycleScope?.cancel()
@ -1412,7 +1423,6 @@ abstract class CoreReaderFragment :
bottomToolbarArrowForward = null
bottomToolbarHome = null
tabRecyclerView = null
snackBarRoot = null
noOpenBookText = null
bottomToolbarToc = null
bottomToolbar = null
@ -1484,16 +1494,17 @@ abstract class CoreReaderFragment :
@Throws(IllegalArgumentException::class)
protected open fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? {
requireNotNull(activityMainRoot)
// requireNotNull(activityMainRoot)
return ToolbarScrollingKiwixWebView(
requireActivity(),
this,
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"),
activityMainRoot as ViewGroup,
requireNotNull(videoView),
null,
// requireNotNull(readerScreenState.value.fullScreenItem.second),
null,
CoreWebViewClient(this, requireNotNull(zimReaderContainer)),
requireNotNull(toolbarContainer),
requireNotNull(bottomToolbar),
// requireNotNull(toolbarContainer),
// requireNotNull(bottomToolbar),
requireNotNull(sharedPreferenceUtil)
)
}
@ -1550,14 +1561,18 @@ abstract class CoreReaderFragment :
notifyItemRemoved(index)
notifyDataSetChanged()
}
snackBarRoot?.let {
it.bringToFront()
Snackbar.make(it, R.string.tab_closed, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { undoButton ->
undoButton.isEnabled = false
restoreDeletedTab(index)
}.addCallback(restoreTabsSnackbarCallback).show()
}
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.tab_closed),
actionLabel = requireActivity().getString(R.string.undo),
snackbarDuration = SnackbarDuration.Long,
actionClick = { restoreDeletedTab(index) },
lifecycleScope = lifecycleScope,
snackBarResult = { result ->
if (result != SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
closeZimBook()
}
}
)
openHomeScreen()
}
@ -1569,8 +1584,12 @@ abstract class CoreReaderFragment :
protected fun exitBook(shouldCloseZimBook: Boolean = true) {
showNoBookOpenViews()
bottomToolbar?.visibility = GONE
actionBar?.title = getString(R.string.reader)
readerScreenState.update {
copy(
shouldShowBottomAppBar = false,
readerScreenTitle = requireActivity().getString(R.string.reader)
)
}
contentFrame?.visibility = GONE
hideProgressBar()
mainMenu?.hideBookSpecificMenuItems()
@ -1586,17 +1605,14 @@ abstract class CoreReaderFragment :
}
protected fun showProgressBarWithProgress(progress: Int) {
progressBar?.apply {
visibility = VISIBLE
show()
this.progress = progress
readerScreenState.update {
copy(pageLoadingItem = true to progress)
}
}
protected fun hideProgressBar() {
progressBar?.apply {
visibility = GONE
hide()
readerScreenState.update {
copy(pageLoadingItem = false to ZERO)
}
}
@ -1617,9 +1633,10 @@ abstract class CoreReaderFragment :
}
webViewList.add(index, it)
tabsAdapter?.notifyDataSetChanged()
snackBarRoot?.let { root ->
Snackbar.make(root, R.string.tab_restored, Snackbar.LENGTH_SHORT).show()
}
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.tab_restored),
lifecycleScope = lifecycleScope
)
setUpWithTextToSpeech(it)
updateBottomToolbarVisibility()
safelyAddWebView(it)
@ -1628,22 +1645,21 @@ abstract class CoreReaderFragment :
private fun safelyAddWebView(webView: KiwixWebView) {
webView.parent?.let { (it as ViewGroup).removeView(webView) }
contentFrame?.addView(webView)
readerScreenState.update {
copy(selectedWebView = webView)
}
}
protected fun selectTab(position: Int) {
currentWebViewIndex = position
contentFrame?.let {
it.removeAllViews()
val webView = safelyGetWebView(position) ?: return@selectTab
safelyAddWebView(webView)
tabsAdapter?.selected = currentWebViewIndex
updateBottomToolbarVisibility()
loadPrefs()
updateUrlFlow()
updateTableOfContents()
updateTitle()
}
val webView = safelyGetWebView(position) ?: return
safelyAddWebView(webView)
tabsAdapter?.selected = currentWebViewIndex
updateBottomToolbarVisibility()
loadPrefs()
updateUrlFlow()
updateTableOfContents()
updateTitle()
}
private fun safelyGetWebView(position: Int): KiwixWebView? =
@ -1667,22 +1683,6 @@ abstract class CoreReaderFragment :
}
}
private fun setupMenu() {
(requireActivity() as MenuHost).addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menu.clear()
mainMenu = createMainMenu(menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean =
mainMenu?.onOptionsItemSelected(menuItem) == true
},
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
}
override fun onFullscreenMenuClicked() {
if (isInFullScreenMode()) {
closeFullScreen()
@ -1846,7 +1846,9 @@ abstract class CoreReaderFragment :
protected open fun openFullScreen() {
(requireActivity() as CoreMainActivity).disableDrawer(false)
toolbarContainer?.visibility = GONE
bottomToolbar?.visibility = GONE
readerScreenState.update {
copy(shouldShowBottomAppBar = false)
}
exitFullscreenButton?.visibility = VISIBLE
exitFullscreenButton?.background?.alpha = 153
val window = requireActivity().window
@ -2038,16 +2040,19 @@ abstract class CoreReaderFragment :
zimReaderSource?.let { openZimFile(it) }
}
} else {
snackBarRoot?.let { snackBarRoot ->
Snackbar.make(snackBarRoot, R.string.request_storage, Snackbar.LENGTH_LONG)
.setAction(R.string.menu_settings) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", requireActivity().packageName, null)
intent.data = uri
startActivity(intent)
}.show()
}
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.request_storage),
actionLabel = requireActivity().getString(R.string.menu_settings),
snackbarDuration = SnackbarDuration.Long,
actionClick = {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", requireActivity().packageName, null)
intent.data = uri
startActivity(intent)
},
lifecycleScope = lifecycleScope
)
}
}
@ -2073,27 +2078,18 @@ abstract class CoreReaderFragment :
webViewList.clear()
tabsAdapter?.notifyDataSetChanged()
openHomeScreen()
snackBarRoot?.let { root ->
root.bringToFront()
Snackbar.make(root, R.string.tabs_closed, Snackbar.LENGTH_LONG).apply {
setAction(R.string.undo) {
it.isEnabled = false // to prevent multiple clicks on this button
setIsCloseAllTabButtonClickable(true)
restoreDeletedTabs()
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.tabs_closed),
actionLabel = requireActivity().getString(R.string.undo),
snackbarDuration = SnackbarDuration.Long,
actionClick = { restoreDeletedTabs() },
lifecycleScope = lifecycleScope,
snackBarResult = { result ->
if (result != SnackbarResult.Dismissed && webViewList.isEmpty() && isAdded) {
closeZimBook()
}
}.addCallback(restoreTabsSnackbarCallback).show()
}
}
private var restoreTabsSnackbarCallback: Snackbar.Callback? = object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
// If the undo button is not clicked and no tabs are left, exit the book and
// clean up resources.
if (event != DISMISS_EVENT_ACTION && webViewList.isEmpty() && isAdded) {
closeZimBook()
}
}
)
}
private fun setIsCloseAllTabButtonClickable(isClickable: Boolean) {
@ -2104,9 +2100,10 @@ abstract class CoreReaderFragment :
if (tempWebViewListForUndo.isNotEmpty()) {
webViewList.addAll(tempWebViewListForUndo)
tabsAdapter?.notifyDataSetChanged()
snackBarRoot?.let { root ->
Snackbar.make(root, R.string.tabs_restored, Snackbar.LENGTH_SHORT).show()
}
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.tabs_restored),
lifecycleScope = lifecycleScope
)
reopenBook()
showTabSwitcher()
setUpWithTextToSpeech(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex])
@ -2117,13 +2114,11 @@ abstract class CoreReaderFragment :
// opens home screen when user closes all tabs
protected fun showNoBookOpenViews() {
noOpenBookButton?.visibility = VISIBLE
noOpenBookText?.visibility = VISIBLE
readerScreenState.update { copy(isNoBookOpenInReader = true) }
}
private fun hideNoBookOpenViews() {
noOpenBookButton?.visibility = GONE
noOpenBookText?.visibility = GONE
readerScreenState.update { copy(isNoBookOpenInReader = false) }
}
@Suppress("MagicNumber")
@ -2145,19 +2140,20 @@ abstract class CoreReaderFragment :
val libKiwixBook = getLibkiwixBook(zimFileReader)
if (isBookmarked) {
repositoryActions?.deleteBookmark(libKiwixBook.id, articleUrl)
snackBarRoot?.snack(R.string.bookmark_removed)
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.bookmark_removed),
lifecycleScope = lifecycleScope
)
} else {
getCurrentWebView()?.title?.let {
repositoryActions?.saveBookmark(
LibkiwixBookmarkItem(it, articleUrl, zimFileReader, libKiwixBook)
)
snackBarRoot?.snack(
stringId = R.string.bookmark_added,
actionStringId = R.string.open,
actionClick = {
goToBookmarks()
Unit
}
readerScreenState.value.snackBarHostState.snack(
requireActivity().getString(R.string.bookmark_added),
lifecycleScope = lifecycleScope,
actionLabel = requireActivity().getString(R.string.open),
actionClick = { goToBookmarks() }
)
}
}
@ -2174,7 +2170,7 @@ abstract class CoreReaderFragment :
}
/**
* Returns the libkiwix book evertime when user saves or remove the bookmark.
* Returns the libkiwix book everytime when user saves or remove the bookmark.
* the object will be created once to avoid creating it multiple times.
*/
private fun getLibkiwixBook(zimFileReader: ZimFileReader): Book {
@ -2289,14 +2285,10 @@ abstract class CoreReaderFragment :
protected fun isInFullScreenMode(): Boolean = sharedPreferenceUtil?.prefFullScreen == true
private fun updateBottomToolbarVisibility() {
bottomToolbar?.let {
if (urlIsValid() &&
tabSwitcherRoot?.visibility != VISIBLE && !isInFullScreenMode()
) {
it.visibility = VISIBLE
} else {
it.visibility = GONE
}
// TODO refactroe this code once we integrate the tabSwitcher
// tabSwitcherRoot?.visibility != VISIBLE && !isInFullScreenMode()
readerScreenState.update {
copy(shouldShowBottomAppBar = !isInFullScreenMode())
}
}
@ -2566,17 +2558,13 @@ abstract class CoreReaderFragment :
* WARNING: If modifying this method, ensure thorough testing with custom apps
* to verify proper functionality.
*/
protected open fun createMainMenu(menu: Menu?): MainMenu? =
menu?.let {
menuFactory?.create(
it,
webViewList,
urlIsValid(),
menuClickListener = this,
disableReadAloud = false,
disableTabs = false
)
}
protected open fun createMainMenu(menu: Menu?): ReaderMenuState? =
ReaderMenuState(
this,
disableReadAloud = false,
disableTabs = false,
disableSearch = false
)
protected fun urlIsValid(): Boolean = getCurrentWebView()?.url != null
@ -2588,7 +2576,7 @@ abstract class CoreReaderFragment :
painter?.update(
getCurrentWebView(),
::shouldActivateNightMode,
videoView
readerScreenState.value.fullScreenItem.second
)
}
@ -2878,11 +2866,14 @@ abstract class CoreReaderFragment :
{
if (isOpenNewTabInBackground) {
newTabInBackground(url)
snackBarRoot?.snack(
stringId = R.string.new_tab_snack_bar,
actionStringId = R.string.open,
readerScreenState.value.snackBarHostState.snack(
message = requireActivity().getString(R.string.new_tab_snack_bar),
lifecycleScope = lifecycleScope,
actionLabel = requireActivity().getString(R.string.open),
actionClick = {
if (webViewList.size > 1) selectTab(webViewList.size - 1)
if (webViewList.size > 1) {
selectTab(webViewList.size - 1)
}
}
)
} else {
@ -3104,6 +3095,8 @@ abstract class CoreReaderFragment :
* when handling invalid JSON scenarios.
*/
abstract fun restoreViewStateOnInvalidWebViewHistory()
abstract fun getBottomNavigationView(): BottomNavigationView?
}
enum class RestoreOrigin {

View File

@ -0,0 +1,156 @@
/*
* Kiwix Android
* Copyright (c) 2025 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.main.reader
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
const val READ_ALOUD_MENU_ITEM_TESTING_TAG = "readAloudMenuItemTestingTag"
const val TAKE_NOTE_MENU_ITEM_TESTING_TAG = "takeNoteMenuItemTestingTag"
const val FULL_SCREEN_MENU_ITEM_TESTING_TAG = "fullScreenMenuItemTestingTag"
const val RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG = "randomArticleMenuItemTestingTag"
const val TAB_MENU_ITEM_TESTING_TAG = "tabMenuItemTestingTag"
@Stable
class ReaderMenuState(
private val menuClickListener: MenuClickListener,
private val disableReadAloud: Boolean = false,
private val disableTabs: Boolean = false,
private val disableSearch: Boolean = false
) {
interface MenuClickListener {
fun onTabMenuClicked()
fun onHomeMenuClicked()
fun onAddNoteMenuClicked()
fun onRandomArticleMenuClicked()
fun onReadAloudMenuClicked()
fun onFullscreenMenuClicked()
fun onSearchMenuClickedMenuClicked()
}
var isInTabSwitcher by mutableStateOf(false)
private set
var isReadingAloud by mutableStateOf(false)
private set
var webViewCount by mutableStateOf(0)
var urlIsValid by mutableStateOf(false)
var zimFileReaderAvailable by mutableStateOf(false)
fun onTabsChanged(count: Int) {
webViewCount = count
}
fun onUrlValidityChanged(valid: Boolean) {
urlIsValid = valid
}
fun onZimFileReaderAvailable(available: Boolean) {
zimFileReaderAvailable = available
}
fun onTextToSpeechStarted() {
isReadingAloud = true
}
fun onTextToSpeechStopped() {
isReadingAloud = false
}
fun exitTabSwitcher() {
isInTabSwitcher = false
}
@Suppress("LongMethod", "MagicNumber")
fun getActionMenuItems(): List<ActionMenuItem> {
if (isInTabSwitcher) {
return emptyList()
}
val list = mutableListOf<ActionMenuItem>()
if (!disableSearch && urlIsValid) {
list += ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label,
onClick = { menuClickListener.onSearchMenuClickedMenuClicked() },
isInOverflow = false,
testingTag = SEARCH_ICON_TESTING_TAG
)
}
if (!disableTabs) {
val tabLabel = if (webViewCount > 99) ":D" else "$webViewCount"
list += ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Add),
contentDescription = R.string.switch_tabs,
onClick = {
isInTabSwitcher = true
menuClickListener.onTabMenuClicked()
},
isInOverflow = false,
iconButtonText = tabLabel,
testingTag = TAB_MENU_ITEM_TESTING_TAG
)
}
if (urlIsValid) {
list += listOf(
ActionMenuItem(
icon = IconItem.Drawable(R.drawable.ic_add_note),
contentDescription = R.string.take_notes,
onClick = { menuClickListener.onAddNoteMenuClicked() },
testingTag = TAKE_NOTE_MENU_ITEM_TESTING_TAG
),
ActionMenuItem(
contentDescription = R.string.menu_random_article,
onClick = { menuClickListener.onRandomArticleMenuClicked() },
testingTag = RANDOM_ARTICLE_MENU_ITEM_TESTING_TAG
),
ActionMenuItem(
contentDescription = R.string.menu_full_screen,
onClick = { menuClickListener.onFullscreenMenuClicked() },
testingTag = FULL_SCREEN_MENU_ITEM_TESTING_TAG
)
)
if (!disableReadAloud) {
list += ActionMenuItem(
contentDescription = if (isReadingAloud) R.string.menu_read_aloud_stop else R.string.menu_read_aloud,
onClick = {
isReadingAloud = !isReadingAloud
menuClickListener.onReadAloudMenuClicked()
},
testingTag = READ_ALOUD_MENU_ITEM_TESTING_TAG
)
}
}
return list
}
}

View File

@ -36,13 +36,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@ -54,23 +57,31 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.dp
import androidx.compose.ui.viewinterop.AndroidView
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.main.DarkModeViewPainter
import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
@ -78,6 +89,8 @@ import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.components.ScrollDirection
import org.kiwix.kiwixmobile.core.ui.components.rememberLazyListScrollListener
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem.Drawable
@ -103,14 +116,29 @@ const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTe
@Composable
fun ReaderScreen(
state: ReaderScreenState,
listState: LazyListState,
actionMenuItems: List<ActionMenuItem>,
navigationIcon: @Composable () -> Unit
) {
val (bottomNavHeight, lazyListState) =
rememberScrollBehavior(state.bottomNavigationHeight, listState)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
KiwixDialogTheme {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = { KiwixAppBar(R.string.note, navigationIcon, actionMenuItems) },
floatingActionButton = { BackToTopFab(state) }
topBar = {
KiwixAppBar(
state.readerScreenTitle,
navigationIcon,
actionMenuItems,
scrollBehavior
)
},
floatingActionButton = { BackToTopFab(state) },
modifier = Modifier
.systemBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(bottom = bottomNavHeight.value)
) { paddingValues ->
Box(
modifier = Modifier
@ -120,15 +148,16 @@ fun ReaderScreen(
if (state.isNoBookOpenInReader) {
NoBookOpenView(state.onOpenLibraryButtonClicked)
} else {
ShowZIMFileContent(state)
ShowProgressBarIfZIMFilePageIsLoading(state)
ShowZIMFileContent(state.kiwixWebViewList)
TtsControls(state)
BottomAppBarOfReaderScreen(
state.bookmarkButtonItem,
state.previousPageButtonItem,
state.onHomeButtonClick,
state.nextPageButtonItem,
state.onTocClick
state.onTocClick,
state.shouldShowBottomAppBar
)
ShowFullScreenView(state)
}
@ -139,9 +168,14 @@ fun ReaderScreen(
}
@Composable
private fun ShowZIMFileContent(kiwixWebViewList: List<KiwixWebView>) {
if (kiwixWebViewList.isNotEmpty()) {
AndroidView({ kiwixWebViewList[0] }, modifier = Modifier.fillMaxSize())
private fun ShowZIMFileContent(state: ReaderScreenState) {
state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) {
AndroidView(
factory = { selectedWebView },
modifier = Modifier.fillMaxSize()
)
}
}
}
@ -158,7 +192,7 @@ private fun BoxScope.ShowProgressBarIfZIMFilePageIsLoading(state: ReaderScreenSt
ContentLoadingProgressBar(
modifier = Modifier
.testTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG)
.align(Alignment.CenterEnd),
.align(Alignment.TopCenter),
progressBarStyle = ProgressBarStyle.HORIZONTAL,
progress = state.pageLoadingItem.second
)
@ -240,17 +274,22 @@ private fun BackToTopFab(state: ReaderScreenState) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BottomAppBarOfReaderScreen(
private fun BoxScope.BottomAppBarOfReaderScreen(
bookmarkButtonItem: Triple<() -> Unit, () -> Unit, Drawable>,
previousPageButtonItem: Pair<() -> Unit, () -> Unit>,
previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onHomeButtonClick: () -> Unit,
nextPageButtonItem: Pair<() -> Unit, () -> Unit>,
onTocClick: () -> Unit
nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
onTocClick: () -> Unit,
shouldShowBottomAppBar: Boolean
) {
if (!shouldShowBottomAppBar) return
BottomAppBar(
containerColor = Black,
contentColor = White
contentColor = White,
modifier = Modifier.align(Alignment.BottomCenter),
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
) {
Row(
modifier = Modifier
@ -271,6 +310,7 @@ private fun BottomAppBarOfReaderScreen(
onClick = previousPageButtonItem.first,
onLongClick = previousPageButtonItem.second,
buttonIcon = Drawable(R.drawable.ic_keyboard_arrow_left_24dp),
shouldEnable = previousPageButtonItem.third,
contentDescription = stringResource(R.string.go_to_previous_page)
)
// Home Icon(to open the home page of ZIM file)
@ -284,6 +324,7 @@ private fun BottomAppBarOfReaderScreen(
onClick = nextPageButtonItem.first,
onLongClick = nextPageButtonItem.second,
buttonIcon = Drawable(R.drawable.ic_keyboard_arrow_right_24dp),
shouldEnable = nextPageButtonItem.third,
contentDescription = stringResource(R.string.go_to_next_page)
)
// Toggle Icon(to open the table of content in right side bar)
@ -301,11 +342,13 @@ private fun BottomAppBarButtonIcon(
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
buttonIcon: IconItem,
shouldEnable: Boolean = true,
contentDescription: String
) {
IconButton(
onClick = onClick,
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick)
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick),
enabled = shouldEnable
) {
Icon(
buttonIcon.toPainter(),
@ -454,3 +497,30 @@ fun TabItemView(
}
}
}
@Composable
fun rememberScrollBehavior(
bottomNavigationHeight: Int,
listState: LazyListState,
): Pair<MutableState<Dp>, LazyListState> {
val bottomNavHeightInDp = with(LocalDensity.current) { bottomNavigationHeight.toDp() }
val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) }
val lazyListState = rememberLazyListScrollListener(
lazyListState = listState,
onScrollChanged = { direction ->
when (direction) {
ScrollDirection.SCROLL_UP -> {
bottomNavHeight.value = bottomNavHeightInDp
}
ScrollDirection.SCROLL_DOWN -> {
bottomNavHeight.value = ZERO.dp
}
ScrollDirection.IDLE -> {}
}
}
)
return bottomNavHeight to lazyListState
}

View File

@ -83,6 +83,10 @@ data class ReaderScreenState(
* To show in the tabs view.
*/
val kiwixWebViewList: List<KiwixWebView>,
/**
* Manages the showing of current selected webView.
*/
val selectedWebView: KiwixWebView?,
/**
* Handles the (UI, and clicks) for bookmark button in reader bottom toolbar.
*
@ -95,11 +99,12 @@ data class ReaderScreenState(
/**
* Handles the clicks of previous page button in reader bottom toolbar.
*
* A [Pair] containing:
* A [Triple] containing:
* - [Unit]: Handles the normal click of button(For going to previous page).
* - [Unit]: Handles the long click of button(For showing the previous pages history).
* - [Boolean]: Handles the button should enable or not.
*/
val previousPageButtonItem: Pair<() -> Unit, () -> Unit>,
val previousPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
/**
* Handles the click to open home page of ZIM file button click in reader bottom toolbar.
*/
@ -110,11 +115,21 @@ data class ReaderScreenState(
* A [Pair] containing:
* - [Unit]: Handles the normal click of button(For going to next page).
* - [Unit]: Handles the long click of button(For showing the next pages history).
* - [Boolean]: Handles the button should enable or not.
*/
val nextPageButtonItem: Pair<() -> Unit, () -> Unit>,
val nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
/**
* Handles the click to open right sidebar button click in reader bottom toolbar.
*/
val onTocClick: () -> Unit,
val onCloseAllTabs: () -> Unit
val onCloseAllTabs: () -> Unit,
/**
* Stores the height of the bottom navigation bar in pixels.
*/
val bottomNavigationHeight: Int,
/**
* Manages the showing of Reader's [BottomAppBarOfReaderScreen].
*/
val shouldShowBottomAppBar: Boolean,
val readerScreenTitle: String
)

View File

@ -88,7 +88,7 @@ fun PageScreen(
topBar = {
Column {
KiwixAppBar(
titleId = state.screenTitle,
title = stringResource(state.screenTitle),
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems,
searchBar = searchBarIfActive(state)

View File

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

View File

@ -260,7 +260,7 @@ class SearchFragment : BaseFragment() {
searchViewModel.actions.trySend(ClickedSearchInText).isSuccess
},
testingTag = FIND_IN_PAGE_TESTING_TAG,
iconButtonText = R.string.menu_search_in_text,
iconButtonText = requireActivity().getString(R.string.menu_search_in_text),
isEnabled = findInPageMenuItem.value.first
)
} else {

View File

@ -87,7 +87,7 @@ fun SearchScreen(
Scaffold(
topBar = {
KiwixAppBar(
titleId = R.string.empty_string,
title = stringResource(R.string.empty_string),
navigationIcon = searchScreenState.navigationIcon,
actionMenuItems = actionMenuItemList,
searchBar = {

View File

@ -117,7 +117,7 @@ fun SettingsScreen(
Scaffold(
topBar = {
KiwixAppBar(
titleId = R.string.menu_settings,
title = stringResource(R.string.menu_settings),
navigationIcon = navigationIcon
)
}

View File

@ -18,7 +18,6 @@
package org.kiwix.kiwixmobile.core.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@ -29,6 +28,10 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -67,7 +70,7 @@ const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KiwixAppBar(
@StringRes titleId: Int,
title: String,
navigationIcon: @Composable () -> Unit,
actionMenuItems: List<ActionMenuItem> = emptyList(),
topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
@ -76,7 +79,7 @@ fun KiwixAppBar(
) {
KiwixTheme {
TopAppBar(
title = { AppBarTitleSection(titleId, searchBar) },
title = { AppBarTitleSection(title, searchBar) },
navigationIcon = navigationIcon,
actions = { ActionMenu(actionMenuItems) },
scrollBehavior = topAppBarScrollBehavior,
@ -95,7 +98,7 @@ fun KiwixAppBar(
@Suppress("ComposableLambdaParameterNaming")
@Composable
private fun AppBarTitleSection(
@StringRes titleId: Int,
title: String,
searchBar: (@Composable () -> Unit)? = null
) {
Box(
@ -107,14 +110,14 @@ private fun AppBarTitleSection(
searchBar?.let {
it()
} ?: run {
AppBarTitle(titleId)
AppBarTitle(title)
}
}
}
@Composable
private fun AppBarTitle(
@StringRes titleId: Int
title: String
) {
val appBarTitleColor = if (isSystemInDarkTheme()) {
MineShaftGray350
@ -122,7 +125,7 @@ private fun AppBarTitle(
White
}
Text(
text = stringResource(titleId),
text = title,
color = appBarTitleColor,
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
@ -132,10 +135,14 @@ private fun AppBarTitle(
)
}
@Suppress("LongMethod")
@Composable
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
var overflowExpanded by remember { mutableStateOf(false) }
Row {
actionMenuItems.forEach { menuItem ->
val (mainActions, overflowActions) = actionMenuItems.partition { !it.isInOverflow }
mainActions.forEach { menuItem ->
val modifier = menuItem.modifier.testTag(menuItem.testingTag)
// If icon is not null show the icon.
menuItem.icon?.let {
@ -158,7 +165,7 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
modifier = modifier
) {
Text(
text = stringResource(id = menuItem.iconButtonText).uppercase(),
text = menuItem.iconButtonText.uppercase(),
color = if (menuItem.isEnabled) Color.White else Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -166,6 +173,32 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
}
}
}
if (overflowActions.isNotEmpty()) {
IconButton(onClick = { overflowExpanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = White
)
}
}
DropdownMenu(
expanded = overflowExpanded,
onDismissRequest = { overflowExpanded = false }
) {
overflowActions.forEach { menuItem ->
DropdownMenuItem(
text = {
Text(text = menuItem.iconButtonText)
},
onClick = {
overflowExpanded = false
menuItem.onClick()
},
enabled = menuItem.isEnabled
)
}
}
}
}

View File

@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.core.ui.models
import androidx.annotation.StringRes
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.theme.White
data class ActionMenuItem(
@ -30,7 +29,8 @@ data class ActionMenuItem(
val onClick: () -> Unit,
val iconTint: Color = White,
val isEnabled: Boolean = true,
@StringRes val iconButtonText: Int = R.string.empty_string,
val iconButtonText: String = "",
val testingTag: String,
val modifier: Modifier = Modifier
val modifier: Modifier = Modifier,
val isInOverflow: Boolean = false
)

View File

@ -181,7 +181,7 @@ object ComposeDimens {
val CATEGORY_TITLE_TEXT_SIZE = 14.sp
// Reader screen dimes
val READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT = 48.dp
val READER_BOTTOM_APP_BAR_LAYOUT_HEIGHT = 40.dp
val READER_BOTTOM_APP_BAR_BUTTON_ICON_SIZE = 30.dp
const val TTS_BUTTONS_CONTROL_ALPHA = 0.6f
val CLOSE_ALL_TAB_BUTTON_BOTTOM_PADDING = 24.dp

View File

@ -215,6 +215,10 @@ class CustomMainActivity : CoreMainActivity() {
activityCustomMainBinding.root.addView(getDialogHostComposeView(alertDialogShower), 0)
}
override fun toggleBottomNavigation(isVisible: Boolean) {
// Do nothing as we do not have the bottomNavigationView in custom apps.
}
// Outdated shortcut id(new_tab)
// Remove if the application has the outdated shortcut.
private fun removeOutdatedIdShortcuts() {

View File

@ -31,6 +31,7 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R.dimen
import org.kiwix.kiwixmobile.core.base.BaseActivity
@ -38,7 +39,7 @@ import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable
import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.reader.ReaderMenuState
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
@ -157,6 +158,9 @@ 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.
@ -304,19 +308,13 @@ class CustomReaderFragment : CoreReaderFragment() {
* provided configuration. It takes into account whether read aloud and tabs are enabled or disabled
* and creates the menu accordingly.
*/
override fun createMainMenu(menu: Menu?): MainMenu? {
return menu?.let {
menuFactory?.create(
it,
webViewList,
urlIsValid(),
this,
BuildConfig.DISABLE_READ_ALOUD,
BuildConfig.DISABLE_TABS,
BuildConfig.DISABLE_TITLE
)
}
}
override fun createMainMenu(menu: Menu?): ReaderMenuState? =
ReaderMenuState(
this,
disableReadAloud = BuildConfig.DISABLE_READ_ALOUD,
disableTabs = BuildConfig.DISABLE_TABS,
disableSearch = BuildConfig.DISABLE_TITLE
)
/**
* Overrides the method to control the functionality of showing the "Open In New Tab" dialog.