Fixed: The system bar was not correctly showing the icons.

* Fixed: When scrolling, the BottomAppBar was appearing behind the navigation buttons.
* Fixed: The hint in the table of contents was not showing on first-time app install.
* Fixed: Pressing the back button was immediately exiting the app when the table of contents drawer was open.
* Created the `CustomNavGraph` for custom app navigation and refactored related code.
* Refactored the logic to dynamically disable the left drawer when a custom app is configured not to show it.
* Simplified the code for enabling/disabling the sidebar.
* Resolved all lint and Detekt errors.
This commit is contained in:
MohitMaliFtechiz 2025-07-30 13:38:58 +05:30
parent 8afaa8be52
commit 4d97eeebc8
14 changed files with 474 additions and 385 deletions

View File

@ -23,7 +23,6 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.MenuItem
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@ -50,7 +49,6 @@ import org.kiwix.kiwixmobile.BuildConfig
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R.drawable import org.kiwix.kiwixmobile.core.R.drawable
import org.kiwix.kiwixmobile.core.R.id
import org.kiwix.kiwixmobile.core.R.mipmap import org.kiwix.kiwixmobile.core.R.mipmap
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
@ -63,7 +61,6 @@ import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.DrawerMenuItem import org.kiwix.kiwixmobile.core.main.DrawerMenuItem
import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
import org.kiwix.kiwixmobile.kiwixActivityComponent import org.kiwix.kiwixmobile.kiwixActivityComponent
import org.kiwix.kiwixmobile.ui.KiwixDestination import org.kiwix.kiwixmobile.ui.KiwixDestination
@ -72,25 +69,12 @@ import javax.inject.Inject
const val ACTION_GET_CONTENT = "GET_CONTENT" const val ACTION_GET_CONTENT = "GET_CONTENT"
const val OPENING_ZIM_FILE_DELAY = 300L const val OPENING_ZIM_FILE_DELAY = 300L
const val GET_CONTENT_SHORTCUT_ID = "get_content_shortcut" const val GET_CONTENT_SHORTCUT_ID = "get_content_shortcut"
const val KIWIX_BOTTOM_BAR_ANIMATION_DURATION = 250L
class KiwixMainActivity : CoreMainActivity() { class KiwixMainActivity : CoreMainActivity() {
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
override val cachedComponent by lazy { kiwixActivityComponent } override val cachedComponent by lazy { kiwixActivityComponent }
override val searchFragmentRoute: String = KiwixDestination.Search.route override val searchFragmentRoute: String = KiwixDestination.Search.route
// override val drawerContainerLayout: DrawerLayout by lazy {
// // activityKiwixMainBinding.navigationContainer
// }
// override val drawerNavView: NavigationView by lazy {
// activityKiwixMainBinding.drawerNavView
// }
// override val readerTableOfContentsDrawer: NavigationView by lazy {
// activityKiwixMainBinding.readerDrawerNavView
// }
@Inject lateinit var libkiwixBookOnDisk: LibkiwixBookOnDisk @Inject lateinit var libkiwixBookOnDisk: LibkiwixBookOnDisk
override val mainActivity: AppCompatActivity by lazy { this } override val mainActivity: AppCompatActivity by lazy { this }
@ -288,14 +272,6 @@ class KiwixMainActivity : CoreMainActivity() {
navigate(KiwixDestination.Reader.createRoute(zimFileUri = path)) navigate(KiwixDestination.Reader.createRoute(zimFileUri = path))
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
id.menu_host_books -> openZimHostFragment()
else -> return super.onNavigationItemSelected(item)
}
return true
}
override val zimHostDrawerMenuItem: DrawerMenuItem? = DrawerMenuItem( override val zimHostDrawerMenuItem: DrawerMenuItem? = DrawerMenuItem(
title = CoreApp.instance.getString(string.menu_wifi_hotspot), title = CoreApp.instance.getString(string.menu_wifi_hotspot),
iconRes = drawable.ic_mobile_screen_share_24px, iconRes = drawable.ic_mobile_screen_share_24px,
@ -323,7 +299,7 @@ class KiwixMainActivity : CoreMainActivity() {
override val aboutAppDrawerMenuItem: DrawerMenuItem? = null override val aboutAppDrawerMenuItem: DrawerMenuItem? = null
private fun openZimHostFragment() { private fun openZimHostFragment() {
disableDrawer() disableLeftDrawer()
handleDrawerOnNavigation() handleDrawerOnNavigation()
navigate(KiwixDestination.ZimHost.route) navigate(KiwixDestination.ZimHost.route)
} }
@ -336,10 +312,6 @@ class KiwixMainActivity : CoreMainActivity() {
ShortcutManagerCompat.addDynamicShortcuts(this, dynamicShortcutList()) ShortcutManagerCompat.addDynamicShortcuts(this, dynamicShortcutList())
} }
override fun setDialogHostToActivity(alertDialogShower: AlertDialogShower) {
// activityKiwixMainBinding.root.addView(getDialogHostComposeView(alertDialogShower), 0)
}
override fun openSearch(searchString: String, isOpenedFromTabView: Boolean, isVoice: Boolean) { override fun openSearch(searchString: String, isOpenedFromTabView: Boolean, isVoice: Boolean) {
navigate( navigate(
KiwixDestination.Search.createRoute( KiwixDestination.Search.createRoute(

View File

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.material3.BottomAppBarScrollBehavior
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
@ -51,6 +52,7 @@ import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.ui.KiwixDestination import org.kiwix.kiwixmobile.ui.KiwixDestination
import org.kiwix.kiwixmobile.ui.KiwixNavGraph import org.kiwix.kiwixmobile.ui.KiwixNavGraph
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun KiwixMainActivityScreen( fun KiwixMainActivityScreen(
@ -83,28 +85,28 @@ fun KiwixMainActivityScreen(
// on the hamburger button. // on the hamburger button.
(currentRoute != KiwixDestination.Reader.route || leftDrawerState.isOpen) (currentRoute != KiwixDestination.Reader.route || leftDrawerState.isOpen)
) { ) {
Box { Scaffold(
Scaffold( bottomBar = {
bottomBar = { if (shouldShowBottomBar) {
if (shouldShowBottomBar) { BottomNavigationBar(
BottomNavigationBar(
navController = navController,
bottomAppBarScrollBehaviour = bottomAppBarScrollBehaviour,
navBackStackEntry = navBackStackEntry,
leftDrawerState = leftDrawerState,
uiCoroutineScope = uiCoroutineScope
)
}
},
modifier = Modifier.fillMaxSize()
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
KiwixNavGraph(
navController = navController, navController = navController,
startDestination = startDestination, bottomAppBarScrollBehaviour = bottomAppBarScrollBehaviour,
modifier = Modifier.fillMaxSize() navBackStackEntry = navBackStackEntry,
leftDrawerState = leftDrawerState,
uiCoroutineScope = uiCoroutineScope
) )
} }
},
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
KiwixNavGraph(
navController = navController,
startDestination = startDestination,
modifier = Modifier.fillMaxSize()
)
} }
} }
} }

View File

@ -113,7 +113,6 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
private const val WAS_IN_ACTION_MODE = "WAS_IN_ACTION_MODE" private const val WAS_IN_ACTION_MODE = "WAS_IN_ACTION_MODE"
private const val MATERIAL_BOTTOM_VIEW_ENTER_ANIMATION_DURATION = 225L
const val LOCAL_FILE_TRANSFER_MENU_BUTTON_TESTING_TAG = "localFileTransferMenuButtonTestingTag" const val LOCAL_FILE_TRANSFER_MENU_BUTTON_TESTING_TAG = "localFileTransferMenuButtonTestingTag"
@Suppress("LargeClass") @Suppress("LargeClass")

View File

@ -26,10 +26,8 @@ import android.view.MenuInflater
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
@ -37,7 +35,6 @@ import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCall import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCall
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setupDrawerToggle
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.extensions.snack import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
@ -79,7 +76,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
}) })
} }
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
activity.setupDrawerToggle(true) activity.enableLeftDrawer()
openPageInBookFromNavigationArguments() openPageInBookFromNavigationArguments()
} }
@ -147,11 +144,6 @@ class KiwixReaderFragment : CoreReaderFragment() {
openZimFile(zimReaderSource) openZimFile(zimReaderSource)
} }
override fun loadDrawerViews() {
// drawerLayout = requireActivity().findViewById(R.id.navigation_container)
// tableDrawerRightContainer = requireActivity().findViewById(R.id.reader_drawer_nav_view)
}
override fun openHomeScreen() { override fun openHomeScreen() {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
if (webViewList.isEmpty()) { if (webViewList.isEmpty()) {
@ -174,9 +166,8 @@ class KiwixReaderFragment : CoreReaderFragment() {
* @see closeAllTabs * @see closeAllTabs
*/ */
override fun hideTabSwitcher(shouldCloseZimBook: Boolean) { override fun hideTabSwitcher(shouldCloseZimBook: Boolean) {
activity?.setupDrawerToggle(true) enableLeftDrawer()
(requireActivity() as CoreMainActivity).showBottomAppBar() (requireActivity() as CoreMainActivity).showBottomAppBar()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
if (webViewList.isEmpty()) { if (webViewList.isEmpty()) {
readerMenuState?.hideTabSwitcher() readerMenuState?.hideTabSwitcher()
exitBook(shouldCloseZimBook) exitBook(shouldCloseZimBook)

View File

@ -74,6 +74,7 @@ import org.kiwix.kiwixmobile.nav.destination.reader.KiwixReaderFragment
import org.kiwix.kiwixmobile.settings.KiwixSettingsFragment import org.kiwix.kiwixmobile.settings.KiwixSettingsFragment
import org.kiwix.kiwixmobile.webserver.ZimHostFragment import org.kiwix.kiwixmobile.webserver.ZimHostFragment
@Suppress("LongMethod")
@Composable @Composable
fun KiwixNavGraph( fun KiwixNavGraph(
navController: NavHostController, navController: NavHostController,

View File

@ -95,9 +95,6 @@ object ActivityExtensions {
val Activity.cachedComponent: CoreActivityComponent val Activity.cachedComponent: CoreActivityComponent
get() = coreMainActivity.cachedComponent get() = coreMainActivity.cachedComponent
fun Activity.setupDrawerToggle(shouldEnableRightDrawer: Boolean = false) =
coreMainActivity.setupDrawerToggle(shouldEnableRightDrawer)
fun Activity.navigate(route: String, navOptions: NavOptions? = null) { fun Activity.navigate(route: String, navOptions: NavOptions? = null) {
coreMainActivity.navigate(route, navOptions) coreMainActivity.navigate(route, navOptions)
} }

View File

@ -21,16 +21,13 @@ package org.kiwix.kiwixmobile.core.extensions
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -130,31 +127,3 @@ fun View.closeFullScreenMode(window: Window) {
clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
} }
} }
/**
* Applies edge-to-edge insets to the current view by adjusting its margins
* to account for system bars and display cutouts (e.g., status bar, navigation bar, and notches).
*
* This method ensures that the view avoids overlapping with system UI components by dynamically
* setting margins based on the insets provided by the system.
*
* Usage: Call this method on any view to apply edge-to-edge handling.
*/
fun View?.applyEdgeToEdgeInsets() {
this?.let {
ViewCompat.setOnApplyWindowInsetsListener(it) { view, windowInsets ->
val systemBarsInsets =
windowInsets.getInsets(
WindowInsetsCompat.Type.displayCutout() or
WindowInsetsCompat.Type.systemBars()
)
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBarsInsets.top
leftMargin = systemBarsInsets.left
bottomMargin = systemBarsInsets.bottom
rightMargin = systemBarsInsets.right
}
WindowInsetsCompat.CONSUMED
}
}
}

View File

@ -127,17 +127,13 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
/** /**
* Manages the enabling/disabling the left drawer * Manages the enabling/disabling the left drawer
*/ */
protected val enableLeftDrawer = mutableStateOf(true) val enableLeftDrawer = mutableStateOf(true)
/** /**
* For managing the the showing/hiding the bottomAppBar when scrolling. * For managing the the showing/hiding the bottomAppBar when scrolling.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
var bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior? = null var bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior? = null
// abstract val drawerContainerLayout: DrawerLayout
// abstract val drawerNavView: NavigationView
// abstract val readerTableOfContentsDrawer: NavigationView
abstract val bookmarksFragmentRoute: String abstract val bookmarksFragmentRoute: String
abstract val settingsFragmentRoute: String abstract val settingsFragmentRoute: String
abstract val historyFragmentRoute: String abstract val historyFragmentRoute: String
@ -145,8 +141,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
abstract val helpFragmentRoute: String abstract val helpFragmentRoute: String
abstract val cachedComponent: CoreActivityComponent abstract val cachedComponent: CoreActivityComponent
abstract val topLevelDestinationsRoute: Set<String> abstract val topLevelDestinationsRoute: Set<String>
// abstract val navHostContainer: FragmentContainerView
abstract val mainActivity: AppCompatActivity abstract val mainActivity: AppCompatActivity
abstract val appName: String abstract val appName: String
@ -200,15 +194,11 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
setDialogHostToActivity(alertDialogShower)
externalLinkOpener.setAlertDialogShower(alertDialogShower) externalLinkOpener.setAlertDialogShower(alertDialogShower)
rateDialogHandler.setAlertDialogShower(alertDialogShower) rateDialogHandler.setAlertDialogShower(alertDialogShower)
downloadMonitor.startMonitoringDownload() downloadMonitor.startMonitoringDownload()
stopDownloadServiceIfRunning() stopDownloadServiceIfRunning()
rateDialogHandler.checkForRateDialog(getIconResId()) rateDialogHandler.checkForRateDialog(getIconResId())
// navController.addOnDestinationChangedListener { _, destination, _ ->
// configureActivityBasedOn(destination)
// }
} }
/** /**
@ -302,59 +292,12 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
override fun onSupportNavigateUp(): Boolean = override fun onSupportNavigateUp(): Boolean =
navController.navigateUp() || super.onSupportNavigateUp() navController.navigateUp() || super.onSupportNavigateUp()
open fun setupDrawerToggle(shouldEnableRightDrawer: Boolean = false) { fun enableLeftDrawer() {
enableLeftDrawer.value = true enableLeftDrawer.value = true
// Set the initial contentDescription to the hamburger icon.
// This method is called from various locations after modifying the navigationIcon.
// For example, we previously changed this icon/contentDescription to the "+" button
// when opening the tabSwitcher. After closing the tabSwitcher, we reset the
// contentDescription to the default hamburger icon.
// Todo we will refactore this when migrating the CoreMainActivity.
// toolbar.getToolbarNavigationIcon()?.setToolTipWithContentDescription(
// getString(R.string.open_drawer)
// )
// drawerToggle =
// ActionBarDrawerToggle(
// this,
// drawerContainerLayout,
// R.string.open_drawer,
// R.string.close_drawer
// )
// drawerToggle?.let {
// drawerContainerLayout.addDrawerListener(it)
// it.syncState()
// }
// drawerContainerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
// if (shouldEnableRightDrawer) {
// // Enable the right drawer
// drawerContainerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)
// }
} }
open fun disableDrawer(disableRightDrawer: Boolean = true) { open fun disableLeftDrawer() {
enableLeftDrawer.value = false enableLeftDrawer.value = false
// drawerToggle?.isDrawerIndicatorEnabled = false
// drawerContainerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
// if (disableRightDrawer) {
// // Disable the right drawer
// drawerContainerLayout.setDrawerLockMode(
// DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
// GravityCompat.END
// )
// }
}
open fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_support_kiwix -> openSupportKiwixExternalLink()
R.id.menu_settings -> openSettings()
R.id.menu_help -> openHelpFragment()
R.id.menu_notes -> openNotes()
R.id.menu_history -> openHistory()
R.id.menu_bookmarks_list -> openBookmarks()
else -> return false
}
return true
} }
protected fun openHelpFragment() { protected fun openHelpFragment() {
@ -492,7 +435,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
protected fun handleDrawerOnNavigation() { protected fun handleDrawerOnNavigation() {
closeNavigationDrawer() closeNavigationDrawer()
disableDrawer() disableLeftDrawer()
} }
private fun setMainActivityToCoreApp() { private fun setMainActivityToCoreApp() {
@ -592,7 +535,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
protected abstract fun getIconResId(): Int protected abstract fun getIconResId(): Int
abstract val readerFragmentRoute: String abstract val readerFragmentRoute: String
abstract fun createApplicationShortcuts() abstract fun createApplicationShortcuts()
abstract fun setDialogHostToActivity(alertDialogShower: AlertDialogShower)
abstract fun hideBottomAppBar() abstract fun hideBottomAppBar()
abstract fun showBottomAppBar() abstract fun showBottomAppBar()
} }

View File

@ -68,8 +68,6 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -193,8 +191,6 @@ abstract class CoreReaderFragment :
protected val webViewList = mutableStateListOf<KiwixWebView>() protected val webViewList = mutableStateListOf<KiwixWebView>()
private val webUrlsFlow = MutableStateFlow("") private val webUrlsFlow = MutableStateFlow("")
var drawerLayout: DrawerLayout? = null
@JvmField @JvmField
@Inject @Inject
var sharedPreferenceUtil: SharedPreferenceUtil? = null var sharedPreferenceUtil: SharedPreferenceUtil? = null
@ -490,7 +486,6 @@ abstract class CoreReaderFragment :
} }
handleLocaleCheck() handleLocaleCheck()
initHideBackToTopTimer() initHideBackToTopTimer()
loadDrawerViews()
addFileReader() addFileReader()
activity?.let { activity?.let {
compatCallback = CompatFindActionModeCallback(it) compatCallback = CompatFindActionModeCallback(it)
@ -561,16 +556,28 @@ abstract class CoreReaderFragment :
string.open_drawer string.open_drawer
} }
private fun navigationIconClick() { /**
* Handles clicks on the navigation icon.
* - If the tab switcher is active, triggers the home menu action.
* - Otherwise, toggles the navigation drawer: opens it if closed, closes it if open.
*
* Subclasses like CustomReaderFragment can override this method to provide custom behavior,
* such as disabling the hamburger icon click when the sidebar is configured to be hidden.
*
* WARNING: If modifying this method, ensure thorough testing with custom apps
* to verify proper functionality.
*/
open fun navigationIconClick() {
if (readerMenuState?.isInTabSwitcher == true) { if (readerMenuState?.isInTabSwitcher == true) {
onHomeMenuClicked() onHomeMenuClicked()
return
}
val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else { } else {
val activity = activity as CoreMainActivity activity.openNavigationDrawer()
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else {
activity.openNavigationDrawer()
}
} }
} }
@ -616,13 +623,6 @@ abstract class CoreReaderFragment :
} }
} }
/**
* Abstract method to be implemented by subclasses for loading drawer-related views.
* Subclasses like CustomReaderFragment and KiwixReaderFragment should override this method
* to set up specific views for both the left and right drawers, such as custom containers
* or navigation views.
*/
protected abstract fun loadDrawerViews()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -672,10 +672,9 @@ abstract class CoreReaderFragment :
private fun showTabSwitcher() { private fun showTabSwitcher() {
(requireActivity() as CoreMainActivity).apply { (requireActivity() as CoreMainActivity).apply {
disableDrawer() disableLeftDrawer()
hideBottomAppBar() hideBottomAppBar()
} }
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
readerScreenState.update { readerScreenState.update {
copy( copy(
shouldShowBottomAppBar = false, shouldShowBottomAppBar = false,
@ -716,9 +715,8 @@ abstract class CoreReaderFragment :
* as closing the ZIM book would require reloading the ZIM file, which can be a resource-intensive operation. * as closing the ZIM book would require reloading the ZIM file, which can be a resource-intensive operation.
*/ */
protected open fun hideTabSwitcher(shouldCloseZimBook: Boolean = true) { protected open fun hideTabSwitcher(shouldCloseZimBook: Boolean = true) {
setUpDrawerToggle() enableLeftDrawer()
(requireActivity() as CoreMainActivity).showBottomAppBar() (requireActivity() as CoreMainActivity).showBottomAppBar()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
readerScreenState.update { readerScreenState.update {
copy( copy(
shouldShowBottomAppBar = true, shouldShowBottomAppBar = true,
@ -730,20 +728,15 @@ abstract class CoreReaderFragment :
selectTab(currentWebViewIndex) selectTab(currentWebViewIndex)
} }
open fun setUpDrawerToggle() {
(requireActivity() as CoreMainActivity).setupDrawerToggle(true)
}
/** /**
* Sets the lock mode for the drawer, controlling whether the drawer can be opened or closed. * Enables the activity's left drawer, allowing it to be opened by clicking the hamburger icon.
* Subclasses like CustomReaderFragment override this method to provide custom * Subclasses like CustomReaderFragment can override this method to customize the behavior,
* behavior, such as disabling the sidebar when configured not to show it. * for example, to disable the sidebar when it shouldn't be shown.
* *
* WARNING: If modifying this method, ensure thorough testing with custom apps * WARNING: If you modify this method, thoroughly test it in custom apps to ensure it works correctly.
* to verify proper functionality.
*/ */
protected open fun setDrawerLockMode(lockMode: Int) { open fun enableLeftDrawer() {
drawerLayout?.setDrawerLockMode(lockMode) (requireActivity() as CoreMainActivity).enableLeftDrawer()
} }
private fun goBack() { private fun goBack() {
@ -873,8 +866,8 @@ abstract class CoreReaderFragment :
return FragmentActivityExtensions.Super.ShouldNotCall return FragmentActivityExtensions.Super.ShouldNotCall
} }
drawerLayout?.isDrawerOpen(GravityCompat.END) == true -> { shouldTableOfContentDrawer.value -> {
drawerLayout?.closeDrawers() shouldTableOfContentDrawer.update { false }
return FragmentActivityExtensions.Super.ShouldNotCall return FragmentActivityExtensions.Super.ShouldNotCall
} }
@ -1108,7 +1101,6 @@ abstract class CoreReaderFragment :
private fun unBindViewsAndBinding() { private fun unBindViewsAndBinding() {
compatCallback?.finish() compatCallback?.finish()
compatCallback = null compatCallback = null
drawerLayout = null
} }
private fun updateTableOfContents() { private fun updateTableOfContents() {
@ -1484,13 +1476,12 @@ abstract class CoreReaderFragment :
shouldShowBottomAppBar = false shouldShowBottomAppBar = false
) )
} }
(requireActivity() as CoreMainActivity).disableDrawer(false) (requireActivity() as CoreMainActivity).disableLeftDrawer()
} else { } else {
readerScreenState.update { copy(fullScreenItem = fullScreenItem.copy(first = false)) } readerScreenState.update { copy(fullScreenItem = fullScreenItem.copy(first = false)) }
if (!isInFullScreenMode()) { if (!isInFullScreenMode()) {
readerScreenState.update { copy(shouldShowBottomAppBar = true) } readerScreenState.update { copy(shouldShowBottomAppBar = true) }
setUpDrawerToggle() enableLeftDrawer()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
} }
} }
} }
@ -1498,7 +1489,7 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber") @Suppress("MagicNumber")
protected open fun openFullScreen() { protected open fun openFullScreen() {
(requireActivity() as CoreMainActivity).apply { (requireActivity() as CoreMainActivity).apply {
disableDrawer(false) disableLeftDrawer()
hideBottomAppBar() hideBottomAppBar()
} }
readerScreenState.update { readerScreenState.update {
@ -1518,9 +1509,8 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber") @Suppress("MagicNumber")
open fun closeFullScreen() { open fun closeFullScreen() {
setUpDrawerToggle() enableLeftDrawer()
(requireActivity() as CoreMainActivity).showBottomAppBar() (requireActivity() as CoreMainActivity).showBottomAppBar()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
sharedPreferenceUtil?.putPrefFullScreen(false) sharedPreferenceUtil?.putPrefFullScreen(false)
updateBottomToolbarVisibility() updateBottomToolbarVisibility()
val window = requireActivity().window val window = requireActivity().window
@ -1982,7 +1972,9 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber") @Suppress("MagicNumber")
private fun contentsDrawerHint() { private fun contentsDrawerHint() {
drawerLayout?.postDelayed({ drawerLayout?.openDrawer(GravityCompat.END) }, 500) Handler(Looper.getMainLooper()).postDelayed({
shouldTableOfContentDrawer.update { true }
}, 500)
alertDialogShower?.show(KiwixDialog.ContentsDrawerHint) alertDialogShower?.show(KiwixDialog.ContentsDrawerHint)
} }

View File

@ -169,7 +169,7 @@ const val CLOSE_ALL_TABS_BUTTON_TESTING_TAG = "closeAllTabsButtonTestingTag"
const val TAB_TITLE_TESTING_TAG = "tabTitleTestingTag" const val TAB_TITLE_TESTING_TAG = "tabTitleTestingTag"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming") @Suppress("ComposableLambdaParameterNaming", "LongMethod")
@Composable @Composable
fun ReaderScreen( fun ReaderScreen(
state: ReaderScreenState, state: ReaderScreenState,
@ -305,6 +305,7 @@ private fun ReaderContentLayout(
} }
} }
@Suppress("LongMethod", "UnsafeCallOnNullableType")
@Composable @Composable
fun TableDrawerSheet( fun TableDrawerSheet(
title: String, title: String,
@ -372,7 +373,7 @@ fun TableDrawerSheet(
"document.getElementById('$targetId')?.scrollIntoView();", "document.getElementById('$targetId')?.scrollIntoView();",
null null
) )
delay(100) delay(HUNDERED.toLong())
webViewScrollState.value = ScrollState(selectedWebView?.scrollY ?: ZERO) webViewScrollState.value = ScrollState(selectedWebView?.scrollY ?: ZERO)
scrollToSectionIndex = null scrollToSectionIndex = null
showTableOfContentDrawer.update { false } showTableOfContentDrawer.update { false }

View File

@ -20,178 +20,64 @@ package org.kiwix.kiwixmobile.custom.main
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.compose.rememberNavController
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.navigation.NavigationView
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R.drawable import org.kiwix.kiwixmobile.core.R.drawable
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets
import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getDialogHostComposeView
import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.DrawerMenuItem import org.kiwix.kiwixmobile.core.main.DrawerMenuItem
import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
import org.kiwix.kiwixmobile.custom.BuildConfig import org.kiwix.kiwixmobile.custom.BuildConfig
import org.kiwix.kiwixmobile.custom.R import org.kiwix.kiwixmobile.custom.R
import org.kiwix.kiwixmobile.custom.customActivityComponent import org.kiwix.kiwixmobile.custom.customActivityComponent
import org.kiwix.kiwixmobile.custom.databinding.ActivityCustomMainBinding
class CustomMainActivity : CoreMainActivity() { class CustomMainActivity : CoreMainActivity() {
override val navController: NavController by lazy {
(
supportFragmentManager.findFragmentById(
R.id.custom_nav_controller
) as NavHostFragment
)
.navController
}
override val drawerContainerLayout: DrawerLayout by lazy {
activityCustomMainBinding.customDrawerContainer
}
override val drawerNavView: NavigationView by lazy { activityCustomMainBinding.drawerNavView }
override val readerTableOfContentsDrawer: NavigationView by lazy {
activityCustomMainBinding.activityMainNavView
}
override val navHostContainer by lazy {
activityCustomMainBinding.customNavController
}
override val mainActivity: AppCompatActivity by lazy { this } override val mainActivity: AppCompatActivity by lazy { this }
override val appName: String by lazy { getString(R.string.app_name) } override val appName: String by lazy { getString(R.string.app_name) }
override val searchFragmentResId: Int = R.id.searchFragment override val searchFragmentRoute: String = CustomDestination.Search.route
override val bookmarksFragmentResId: Int = R.id.bookmarksFragment override val bookmarksFragmentRoute: String = CustomDestination.Bookmarks.route
override val settingsFragmentResId: Int = R.id.customSettingsFragment override val settingsFragmentRoute: String = CustomDestination.Settings.route
override val readerFragmentResId: Int = R.id.customReaderFragment override val readerFragmentRoute: String = CustomDestination.Reader.route
override val historyFragmentResId: Int = R.id.historyFragment override val historyFragmentRoute: String = CustomDestination.History.route
override val notesFragmentResId: Int = R.id.notesFragment override val notesFragmentRoute: String = CustomDestination.Notes.route
override val helpFragmentResId: Int = R.id.helpFragment override val helpFragmentRoute: String = CustomDestination.Help.route
override val cachedComponent by lazy { customActivityComponent } override val cachedComponent by lazy { customActivityComponent }
override val topLevelDestinations = override val topLevelDestinationsRoute = setOf(CustomDestination.Reader.route)
setOf(R.id.customReaderFragment)
lateinit var activityCustomMainBinding: ActivityCustomMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
customActivityComponent.inject(this) customActivityComponent.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
activityCustomMainBinding = ActivityCustomMainBinding.inflate(layoutInflater) setContent {
setContentView(activityCustomMainBinding.root) navController = rememberNavController()
activityCustomMainBinding.root.applyEdgeToEdgeInsets() leftDrawerState = rememberDrawerState(DrawerValue.Closed)
uiCoroutineScope = rememberCoroutineScope()
CustomMainActivityScreen(
navController = navController,
leftDrawerContent = leftDrawerMenu,
topLevelDestinationsRoute = topLevelDestinationsRoute,
leftDrawerState = leftDrawerState,
enableLeftDrawer = enableLeftDrawer.value
)
DialogHost(alertDialogShower)
}
if (savedInstanceState != null) { if (savedInstanceState != null) {
return return
} }
} }
override fun onStart() {
super.onStart()
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id !in topLevelDestinations) {
handleDrawerOnNavigation()
}
}
}
override fun setupDrawerToggle(shouldEnableRightDrawer: Boolean) {
super.setupDrawerToggle(shouldEnableRightDrawer)
activityCustomMainBinding.drawerNavView.apply {
/**
* Hide the 'ZimHostFragment' option from the navigation menu
* because we are now using fd (FileDescriptor)
* to read the zim file from the asset folder. Currently,
* 'KiwixServer' is unable to host zim files via fd.
* This feature is temporarily removed for custom apps.
* We will re-enable it for custom apps once the issue is resolved.
* For more info see https://github.com/kiwix/kiwix-android/pull/3516,
* https://github.com/kiwix/kiwix-android/issues/4026
*/
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_host_books)?.isVisible = false
/**
* Hide the `HelpFragment` from custom apps.
* We have not removed the relevant code for `HelpFragment` from custom apps.
* If, in the future, we need to display this for all/some custom apps,
* we can either remove the line below or configure it according to the requirements.
* For more information, see https://github.com/kiwix/kiwix-android/issues/3584
*/
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_help)?.isVisible = false
/**
* If custom app is configured to show the "About app_name app" in navigation
* then show it navigation. "app_name" will be replaced with custom app name.
*/
if (BuildConfig.ABOUT_APP_URL.isNotEmpty()) {
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_about_app)?.apply {
title = getString(
org.kiwix.kiwixmobile.core.R.string.menu_about_app,
getString(R.string.app_name)
)
isVisible = true
}
}
/**
* If custom app is configured to show the "Support app_name" in navigation
* then show it navigation. "app_name" will be replaced with custom app name.
*/
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_support_kiwix)?.apply {
title =
getString(
org.kiwix.kiwixmobile.core.R.string.menu_support_kiwix_for_custom_apps,
getString(R.string.app_name)
)
isVisible = true
}
} else {
/**
* If custom app is not configured to show the "Support app_name" in navigation
* then hide it from navigation.
*/
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_support_kiwix)?.isVisible = false
}
setNavigationItemSelectedListener { item ->
closeNavigationDrawer()
onNavigationItemSelected(item)
}
}
}
/**
* Overrides the method to configure the click behavior of the "About the app"
* and "Support URL" features. When the "About the app" and "Support URL"
* are enabled in a custom app, this function handles those clicks.
*/
override fun onNavigationItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
org.kiwix.kiwixmobile.core.R.id.menu_about_app -> {
if (BuildConfig.ABOUT_APP_URL.isNotEmpty()) {
externalLinkOpener.openExternalUrl(BuildConfig.ABOUT_APP_URL.toUri().browserIntent())
}
true
}
org.kiwix.kiwixmobile.core.R.id.menu_support_kiwix -> {
if (BuildConfig.SUPPORT_URL.isNotEmpty()) {
externalLinkOpener.openExternalUrl(BuildConfig.SUPPORT_URL.toUri().browserIntent(), false)
}
true
}
else -> super.onNavigationItemSelected(item)
}
}
override fun getIconResId() = R.mipmap.ic_launcher override fun getIconResId() = R.mipmap.ic_launcher
/** /**
@ -256,7 +142,10 @@ class CustomMainActivity : CoreMainActivity() {
true, true,
onClick = { onClick = {
closeNavigationDrawer() closeNavigationDrawer()
externalLinkOpener.openExternalUrl(BuildConfig.ABOUT_APP_URL.toUri().browserIntent(), false) externalLinkOpener.openExternalUrl(
BuildConfig.ABOUT_APP_URL.toUri().browserIntent(),
false
)
} }
) )
} else { } else {
@ -281,12 +170,14 @@ class CustomMainActivity : CoreMainActivity() {
ShortcutManagerCompat.addDynamicShortcuts(this, listOf(newTabShortcut)) ShortcutManagerCompat.addDynamicShortcuts(this, listOf(newTabShortcut))
} }
override fun setDialogHostToActivity(alertDialogShower: AlertDialogShower) {
activityCustomMainBinding.root.addView(getDialogHostComposeView(alertDialogShower), 0)
}
override fun openSearch(searchString: String, isOpenedFromTabView: Boolean, isVoice: Boolean) { override fun openSearch(searchString: String, isOpenedFromTabView: Boolean, isVoice: Boolean) {
// TODO implement when refactoring the custom app UI. navigate(
CustomDestination.Search.createRoute(
searchString = searchString,
isOpenedFromTabView = isOpenedFromTabView,
isVoice = isVoice
)
)
} }
override fun hideBottomAppBar() { override fun hideBottomAppBar() {

View File

@ -0,0 +1,78 @@
/*
* 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.custom.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import org.kiwix.kiwixmobile.core.main.DrawerMenuGroup
import org.kiwix.kiwixmobile.core.main.LeftDrawerMenu
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
@Composable
fun CustomMainActivityScreen(
navController: NavHostController,
leftDrawerContent: List<DrawerMenuGroup>,
topLevelDestinationsRoute: Set<String>,
leftDrawerState: DrawerState,
enableLeftDrawer: Boolean,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
KiwixTheme {
ModalNavigationDrawer(
drawerState = leftDrawerState,
drawerContent = {
Column(modifier = Modifier.fillMaxSize()) {
LeftDrawerMenu(leftDrawerContent)
}
},
gesturesEnabled = enableLeftDrawer &&
currentRoute in topLevelDestinationsRoute &&
// Fixing the webView scrolling is lagging when navigation gesture is enabled,
// since navigation consumes the swipes event makes webView lagging.
// However, on reader screen navigation drawer can be opened by clicking
// on the hamburger button.
(currentRoute != CustomDestination.Reader.route || leftDrawerState.isOpen)
) {
Scaffold(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
CustomNavGraph(
navController = navController,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}

View File

@ -0,0 +1,248 @@
/*
* 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.custom.main
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.doOnAttach
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.commit
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import org.kiwix.kiwixmobile.core.main.BOOKMARK_FRAGMENT
import org.kiwix.kiwixmobile.core.main.DOWNLOAD_FRAGMENT
import org.kiwix.kiwixmobile.core.main.FIND_IN_PAGE_SEARCH_STRING
import org.kiwix.kiwixmobile.core.main.HELP_FRAGMENT
import org.kiwix.kiwixmobile.core.main.HISTORY_FRAGMENT
import org.kiwix.kiwixmobile.core.main.NOTES_FRAGMENT
import org.kiwix.kiwixmobile.core.main.PAGE_URL_KEY
import org.kiwix.kiwixmobile.core.main.READER_FRAGMENT
import org.kiwix.kiwixmobile.core.main.SEARCH_FRAGMENT
import org.kiwix.kiwixmobile.core.main.SETTINGS_FRAGMENT
import org.kiwix.kiwixmobile.core.main.SHOULD_OPEN_IN_NEW_TAB
import org.kiwix.kiwixmobile.core.main.ZIM_FILE_URI_KEY
import org.kiwix.kiwixmobile.core.main.reader.SEARCH_ITEM_TITLE_KEY
import org.kiwix.kiwixmobile.core.page.bookmark.BookmarksFragment
import org.kiwix.kiwixmobile.core.page.history.HistoryFragment
import org.kiwix.kiwixmobile.core.page.notes.NotesFragment
import org.kiwix.kiwixmobile.core.search.NAV_ARG_SEARCH_STRING
import org.kiwix.kiwixmobile.core.search.SearchFragment
import org.kiwix.kiwixmobile.core.utils.EXTRA_IS_WIDGET_VOICE
import org.kiwix.kiwixmobile.core.utils.TAG_FROM_TAB_SWITCHER
import org.kiwix.kiwixmobile.custom.download.CustomDownloadFragment
import org.kiwix.kiwixmobile.custom.help.CustomHelpFragment
import org.kiwix.kiwixmobile.custom.settings.CustomSettingsFragment
@Suppress("LongMethod")
@Composable
fun CustomNavGraph(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = CustomDestination.Reader.route,
modifier = modifier
) {
composable(
route = CustomDestination.Reader.route,
arguments = listOf(
navArgument(FIND_IN_PAGE_SEARCH_STRING) {
type = NavType.StringType
defaultValue = ""
},
navArgument(PAGE_URL_KEY) {
type = NavType.StringType
defaultValue = ""
},
navArgument(SHOULD_OPEN_IN_NEW_TAB) {
type = NavType.BoolType
defaultValue = false
}
)
) { backStackEntry ->
val findInPageSearchString =
backStackEntry.arguments?.getString(FIND_IN_PAGE_SEARCH_STRING).orEmpty()
val pageUrl = backStackEntry.arguments?.getString(PAGE_URL_KEY).orEmpty()
val shouldOpenInNewTab = backStackEntry.arguments?.getBoolean(SHOULD_OPEN_IN_NEW_TAB) ?: false
FragmentContainer {
CustomReaderFragment().apply {
arguments = Bundle().apply {
putString(FIND_IN_PAGE_SEARCH_STRING, findInPageSearchString)
putString(PAGE_URL_KEY, pageUrl)
putBoolean(SHOULD_OPEN_IN_NEW_TAB, shouldOpenInNewTab)
}
}
}
}
composable(CustomDestination.History.route) {
FragmentContainer {
HistoryFragment()
}
}
composable(CustomDestination.Notes.route) {
FragmentContainer {
NotesFragment()
}
}
composable(CustomDestination.Bookmarks.route) {
FragmentContainer {
BookmarksFragment()
}
}
composable(CustomDestination.Help.route) {
FragmentContainer {
CustomHelpFragment()
}
}
composable(CustomDestination.Settings.route) {
FragmentContainer {
CustomSettingsFragment()
}
}
composable(CustomDestination.Downloads.route) {
FragmentContainer {
CustomDownloadFragment()
}
}
composable(
route = CustomDestination.Search.route,
arguments = listOf(
navArgument(NAV_ARG_SEARCH_STRING) {
type = NavType.StringType
defaultValue = ""
},
navArgument(TAG_FROM_TAB_SWITCHER) {
type = NavType.BoolType
defaultValue = false
},
navArgument(EXTRA_IS_WIDGET_VOICE) {
type = NavType.BoolType
defaultValue = false
}
)
) { backStackEntry ->
val searchString = backStackEntry.arguments?.getString(NAV_ARG_SEARCH_STRING).orEmpty()
val isOpenedFromTabSwitcher =
backStackEntry.arguments?.getBoolean(TAG_FROM_TAB_SWITCHER) ?: false
val isVoice = backStackEntry.arguments?.getBoolean(EXTRA_IS_WIDGET_VOICE) ?: false
FragmentContainer {
SearchFragment().apply {
arguments = Bundle().apply {
putString(NAV_ARG_SEARCH_STRING, searchString)
putBoolean(TAG_FROM_TAB_SWITCHER, isOpenedFromTabSwitcher)
putBoolean(EXTRA_IS_WIDGET_VOICE, isVoice)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FragmentContainer(
fragmentProvider: () -> Fragment
) {
val context = LocalContext.current
val fragmentManager = remember {
(context as AppCompatActivity).supportFragmentManager
}
val viewId = remember { View.generateViewId() }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
FragmentContainerView(ctx).apply {
id = viewId
doOnAttach {
fragmentManager.commit {
if (fragmentManager.findFragmentById(viewId) == null) {
add(viewId, fragmentProvider())
}
}
}
}
}
)
}
sealed class CustomDestination(val route: String) {
object Reader : CustomDestination(
READER_FRAGMENT +
"?$ZIM_FILE_URI_KEY={$ZIM_FILE_URI_KEY}" +
"&$FIND_IN_PAGE_SEARCH_STRING={$FIND_IN_PAGE_SEARCH_STRING}" +
"&$PAGE_URL_KEY={$PAGE_URL_KEY}" +
"&$SHOULD_OPEN_IN_NEW_TAB={$SHOULD_OPEN_IN_NEW_TAB}" +
"&$SEARCH_ITEM_TITLE_KEY={$SEARCH_ITEM_TITLE_KEY}"
) {
fun createRoute(
zimFileUri: String = "",
findInPageSearchString: String = "",
pageUrl: String = "",
shouldOpenInNewTab: Boolean = false,
searchItemTitle: String = ""
): String {
return READER_FRAGMENT +
"?$ZIM_FILE_URI_KEY=${Uri.encode(zimFileUri)}" +
"&$FIND_IN_PAGE_SEARCH_STRING=${Uri.encode(findInPageSearchString)}" +
"&$PAGE_URL_KEY=${Uri.encode(pageUrl)}" +
"&$SHOULD_OPEN_IN_NEW_TAB=$shouldOpenInNewTab" +
"&$SEARCH_ITEM_TITLE_KEY=${Uri.encode(searchItemTitle)}"
}
}
object History : CustomDestination(HISTORY_FRAGMENT)
object Notes : CustomDestination(NOTES_FRAGMENT)
object Bookmarks : CustomDestination(BOOKMARK_FRAGMENT)
object Help : CustomDestination(HELP_FRAGMENT)
object Settings : CustomDestination(SETTINGS_FRAGMENT)
object Downloads : CustomDestination(DOWNLOAD_FRAGMENT)
object Search : CustomDestination(
SEARCH_FRAGMENT +
"?$NAV_ARG_SEARCH_STRING={$NAV_ARG_SEARCH_STRING}" +
"&$TAG_FROM_TAB_SWITCHER={$TAG_FROM_TAB_SWITCHER}" +
"&$EXTRA_IS_WIDGET_VOICE={$EXTRA_IS_WIDGET_VOICE}"
) {
fun createRoute(
searchString: String = "",
isOpenedFromTabView: Boolean = false,
isVoice: Boolean = false
): String {
return SEARCH_FRAGMENT +
"?$NAV_ARG_SEARCH_STRING=$searchString" +
"&$TAG_FROM_TAB_SWITCHER=$isOpenedFromTabView" +
"&$EXTRA_IS_WIDGET_VOICE=$isVoice"
}
}
}

View File

@ -21,20 +21,19 @@ package org.kiwix.kiwixmobile.custom.main
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.NavOptions
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.extensions.update 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.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.reader.ReaderMenuState import org.kiwix.kiwixmobile.core.main.reader.ReaderMenuState
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
@ -75,10 +74,10 @@ class CustomReaderFragment : CoreReaderFragment() {
} }
if (isAdded) { if (isAdded) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) enableLeftDrawer()
with(activity as AppCompatActivity) { with(activity as AppCompatActivity) {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
setUpDrawerToggle() enableLeftDrawer()
} }
loadPageFromNavigationArguments() loadPageFromNavigationArguments()
if (BuildConfig.DISABLE_EXTERNAL_LINK) { if (BuildConfig.DISABLE_EXTERNAL_LINK) {
@ -151,6 +150,28 @@ class CustomReaderFragment : CoreReaderFragment() {
} }
} }
/**
* Handles clicks on the navigation icon in custom apps.
* - If the tab switcher is active, triggers the home menu action.
* - Otherwise, toggles the navigation drawer: closes it if open; opens it only if the sidebar is enabled.
*
* This override customizes the default behavior by preventing the drawer from opening
* when the sidebar is disabled in the app configuration.
*/
override fun navigationIconClick() {
if (readerMenuState?.isInTabSwitcher == true) {
onHomeMenuClicked()
return
}
val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else if (!BuildConfig.DISABLE_SIDEBAR) {
activity.openNavigationDrawer()
}
}
private fun loadPageFromNavigationArguments() { private fun loadPageFromNavigationArguments() {
val args = CustomReaderFragmentArgs.fromBundle(requireArguments()) val args = CustomReaderFragmentArgs.fromBundle(requireArguments())
if (args.pageUrl.isNotEmpty()) { if (args.pageUrl.isNotEmpty()) {
@ -188,18 +209,16 @@ class CustomReaderFragment : CoreReaderFragment() {
} }
/** /**
* Sets the locking mode for the sidebar in a custom app. If the app is configured not to show the sidebar, * Enables or disables the sidebar in a custom app based on the configuration.
* this function disables the sidebar by locking it in the closed position through the parent class. * If the app is configured to disable the sidebar, this method disables it;
* https://developer.android.com/reference/kotlin/androidx/drawerlayout/widget/DrawerLayout#LOCK_MODE_LOCKED_CLOSED() * otherwise, it enables the sidebar.
*/ */
override fun setDrawerLockMode(lockMode: Int) { override fun enableLeftDrawer() {
super.setDrawerLockMode( if (BuildConfig.DISABLE_SIDEBAR) {
if (BuildConfig.DISABLE_SIDEBAR) { (requireActivity() as CoreMainActivity).disableLeftDrawer()
DrawerLayout.LOCK_MODE_LOCKED_CLOSED } else {
} else { super.enableLeftDrawer()
lockMode }
}
)
} }
/** /**
@ -268,8 +287,13 @@ class CustomReaderFragment : CoreReaderFragment() {
}, },
onNoFilesFound = { onNoFilesFound = {
if (sharedPreferenceUtil?.prefIsTest == false) { if (sharedPreferenceUtil?.prefIsTest == false) {
// TODO refactor this when migrating the custom app in compose val navOptions = NavOptions.Builder()
// (requireActivity() as CoreMainActivity).navigate(R.id.customDownloadFragment) .setPopUpTo(CustomDestination.Reader.route, true)
.build()
(requireActivity() as CoreMainActivity).navigate(
CustomDestination.Downloads.route,
navOptions
)
} }
} }
) )
@ -280,13 +304,6 @@ class CustomReaderFragment : CoreReaderFragment() {
if (!it.isFileExist()) it.createNewFile() if (!it.isFileExist()) it.createNewFile()
} }
@Suppress("DEPRECATION")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_help)?.isVisible = false
menu.findItem(org.kiwix.kiwixmobile.core.R.id.menu_host_books)?.isVisible = false
}
private fun enforcedLanguage(): Boolean { private fun enforcedLanguage(): Boolean {
val currentLocaleCode = Locale.getDefault().toString() val currentLocaleCode = Locale.getDefault().toString()
if (BuildConfig.ENFORCED_LANG.isNotEmpty() && BuildConfig.ENFORCED_LANG != currentLocaleCode) { if (BuildConfig.ENFORCED_LANG.isNotEmpty() && BuildConfig.ENFORCED_LANG != currentLocaleCode) {
@ -304,17 +321,6 @@ class CustomReaderFragment : CoreReaderFragment() {
return false return false
} }
/**
* This method is overridden to set the IDs of the `drawerLayout` and `tableDrawerRightContainer`
* specific to the custom module in the `CoreReaderFragment`. Since we have an app and a custom module,
* and `CoreReaderFragment` is a common class for both modules, we set the IDs of the custom module
* in the parent class to ensure proper integration.
*/
override fun loadDrawerViews() {
drawerLayout = requireActivity().findViewById(R.id.custom_drawer_container)
tableDrawerRightContainer = requireActivity().findViewById(R.id.activity_main_nav_view)
}
/** /**
* Overrides the method to create the main menu for the app. The custom app can be configured to disable * Overrides the method to create the main menu for the app. The custom app can be configured to disable
* features like "read aloud" and "tabs," and this method dynamically generates the menu based on the * features like "read aloud" and "tabs," and this method dynamically generates the menu based on the