Refactored left drawer to support Kiwix and custom apps dynamically; improved right drawer UI.

This commit is contained in:
MohitMaliFtechiz 2025-07-19 19:09:51 +05:30
parent 4e56c2c781
commit 86d9b1266f
7 changed files with 234 additions and 75 deletions

View File

@ -27,6 +27,7 @@ import android.view.MenuItem
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@ -36,13 +37,14 @@ import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.compose.rememberNavController
import eu.mhutti1.utils.storage.StorageDevice
import eu.mhutti1.utils.storage.StorageDeviceUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.BuildConfig
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R.drawable
import org.kiwix.kiwixmobile.core.R.id
import org.kiwix.kiwixmobile.core.R.mipmap
@ -53,6 +55,7 @@ import org.kiwix.kiwixmobile.core.downloader.downloadManager.DOWNLOAD_NOTIFICATI
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.DrawerMenuItem
import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID
import org.kiwix.kiwixmobile.core.main.ZIM_FILE_URI_KEY
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
@ -82,12 +85,6 @@ class KiwixMainActivity : CoreMainActivity() {
// activityKiwixMainBinding.readerDrawerNavView
// }
override val navController: NavController by lazy {
val fragment = supportFragmentManager.findFragmentById(id.nav_host_fragment)
val navHostFragment = requireNotNull(fragment) as NavHostFragment
return@lazy navHostFragment.navController
}
@Inject lateinit var libkiwixBookOnDisk: LibkiwixBookOnDisk
override val mainActivity: AppCompatActivity by lazy { this }
@ -117,15 +114,24 @@ class KiwixMainActivity : CoreMainActivity() {
cachedComponent.inject(this)
super.onCreate(savedInstanceState)
setContent {
navController = rememberNavController()
KiwixMainActivityScreen(
navController = navController,
topLevelDestinations = topLevelDestinations.toList(),
isBottomBarVisible = isBottomBarVisible.value,
leftDrawerContent = { },
leftDrawerContent = rightNavigationDrawerMenuItems,
rightDrawerContent = { }
)
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange)
navController.addOnDestinationChangedListener { _, destination, _ ->
isBottomBarVisible.value = destination.id in topLevelDestinations
if (destination.id !in topLevelDestinations) {
handleDrawerOnNavigation()
}
}
}
}
navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange)
// activityKiwixMainBinding.drawerNavView.apply {
// setupWithNavController(navController)
// setNavigationItemSelectedListener { item ->
@ -207,12 +213,6 @@ class KiwixMainActivity : CoreMainActivity() {
override fun onStart() {
super.onStart()
// navController.addOnDestinationChangedListener { _, destination, _ ->
// isBottomBarVisible.value = destination.id in topLevelDestinations
// if (destination.id !in topLevelDestinations) {
// handleDrawerOnNavigation()
// }
// }
if (sharedPreferenceUtil.showIntro() && !isIntroScreenNotVisible()) {
// navigate(KiwixReaderFragmentDirections.actionReaderFragmentToIntroFragment())
}
@ -337,6 +337,32 @@ class KiwixMainActivity : CoreMainActivity() {
return true
}
override val zimHostDrawerMenuItem: DrawerMenuItem? = DrawerMenuItem(
title = CoreApp.instance.getString(string.menu_wifi_hotspot),
iconRes = drawable.ic_mobile_screen_share_24px,
true,
onClick = { openZimHostFragment() }
)
override val helpDrawerMenuItem: DrawerMenuItem? = DrawerMenuItem(
title = CoreApp.instance.getString(string.menu_help),
iconRes = drawable.ic_help_24px,
true,
onClick = { openHelpFragment() }
)
override val supportDrawerMenuItem: DrawerMenuItem? = DrawerMenuItem(
title = CoreApp.instance.getString(string.menu_support_kiwix),
iconRes = drawable.ic_support_24px,
true,
onClick = { openSupportKiwixExternalLink() }
)
/**
* In kiwix app we are not showing the "About app" item so returning null.
*/
override val aboutAppDrawerMenuItem: DrawerMenuItem? = null
private fun openZimHostFragment() {
disableDrawer()
navigate(R.id.zimHostFragment)

View File

@ -18,8 +18,6 @@
package org.kiwix.kiwixmobile.main
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@ -44,35 +42,31 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.FragmentContainerView
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.fragment.NavHostFragment
import org.kiwix.kiwixmobile.R.drawable
import org.kiwix.kiwixmobile.R.id
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.R.navigation
import org.kiwix.kiwixmobile.core.main.DrawerMenuGroup
import org.kiwix.kiwixmobile.core.main.LeftDrawerMenu
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH
import org.kiwix.kiwixmobile.ui.KiwixNavGraph
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KiwixMainActivityScreen(
navController: NavController,
navController: NavHostController,
topLevelDestinations: List<Int>,
leftDrawerContent: @Composable ColumnScope.() -> Unit,
leftDrawerContent: List<DrawerMenuGroup>,
rightDrawerContent: @Composable ColumnScope.() -> Unit,
isBottomBarVisible: Boolean = true
) {
val rightDrawerState = rememberDrawerState(DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
val scrollingBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
val context = LocalContext.current
val fragmentManager = (context as AppCompatActivity).supportFragmentManager
KiwixTheme {
ModalNavigationDrawer(
drawerContent = {
@ -81,7 +75,7 @@ fun KiwixMainActivityScreen(
.fillMaxHeight()
.width(NAVIGATION_DRAWER_WIDTH)
) {
leftDrawerContent()
LeftDrawerMenu(leftDrawerContent)
}
},
gesturesEnabled = true
@ -99,21 +93,9 @@ fun KiwixMainActivityScreen(
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
// AndroidView to host your FragmentContainerView with nav graph
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
FragmentContainerView(ctx).apply {
id = R.id.nav_host_fragment
if (fragmentManager.findFragmentById(id) == null) {
val navHostFragment = NavHostFragment.create(navigation.kiwix_nav_graph)
fragmentManager.beginTransaction()
.replace(id, navHostFragment)
.setPrimaryNavigationFragment(navHostFragment)
.commitNow()
}
}
}
KiwixNavGraph(
navController = navController,
modifier = Modifier.fillMaxSize()
)
}
}
@ -136,7 +118,7 @@ fun KiwixMainActivityScreen(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomNavigationBar(
navController: NavController,
navController: NavHostController,
scrollBehavior: BottomAppBarScrollBehavior,
topLevelDestinations: List<Int>
) {

View File

@ -29,16 +29,11 @@ import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED
import androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_UNLOCKED
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import com.google.android.material.navigation.NavigationView
import kotlinx.coroutines.CoroutineScope
@ -90,7 +85,12 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
private var drawerToggle: ActionBarDrawerToggle? = null
@Inject lateinit var zimReaderContainer: ZimReaderContainer
abstract val navController: NavController
/**
* We have migrated the UI in compose, so providing the compose based navigation to activity
* is responsibility of child activities such as KiwixMainActivity, and CustomMainActivity.
*/
lateinit var navController: NavHostController
// abstract val drawerContainerLayout: DrawerLayout
// abstract val drawerNavView: NavigationView
@ -102,6 +102,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
abstract val helpFragmentResId: Int
abstract val cachedComponent: CoreActivityComponent
abstract val topLevelDestinations: Set<Int>
// abstract val navHostContainer: FragmentContainerView
abstract val mainActivity: AppCompatActivity
abstract val appName: String
@ -115,7 +116,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
@Suppress("InjectDispatcher")
override fun onCreate(savedInstanceState: Bundle?) {
// setTheme(R.style.KiwixTheme)
setTheme(R.style.KiwixTheme)
super.onCreate(savedInstanceState)
if (!BuildConfig.DEBUG) {
val appContext = applicationContext
@ -322,7 +323,7 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
return true
}
private fun openHelpFragment() {
protected fun openHelpFragment() {
navigate(helpFragmentResId)
handleDrawerOnNavigation()
}
@ -490,6 +491,84 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
navigate(readerFragmentResId, bundleOf(FIND_IN_PAGE_SEARCH_STRING to searchString))
}
private val bookRelatedDrawerGroup = DrawerMenuGroup(
listOfNotNull(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.bookmarks),
iconRes = R.drawable.ic_bookmark_black_24dp,
true,
onClick = { openBookmarks() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.history),
iconRes = R.drawable.ic_history_24px,
true,
onClick = { openHistory() }
),
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.pref_notes),
iconRes = R.drawable.ic_add_note,
true,
onClick = { openNotes() }
),
zimHostDrawerMenuItem
)
)
private val settingDrawerGroup = DrawerMenuGroup(
listOf(
DrawerMenuItem(
title = CoreApp.instance.getString(R.string.menu_settings),
iconRes = R.drawable.ic_settings_24px,
true,
onClick = { openSettings() }
)
)
)
open val helpAndSupportDrawerGroup = DrawerMenuGroup(
listOfNotNull(
helpDrawerMenuItem,
supportDrawerMenuItem,
aboutAppDrawerMenuItem
)
)
/**
* Returns the "Wi-Fi Hotspot" menu item in the left drawer.
* Currently, this feature is only included in the main Kiwix app.
* Custom apps do not include this item.
*/
abstract val zimHostDrawerMenuItem: DrawerMenuItem?
/**
* Returns the "Help" menu item in the left drawer.
* In custom apps, this item is hidden.
* Each app (main Kiwix or custom) provides its own implementation.
*/
abstract val helpDrawerMenuItem: DrawerMenuItem?
/**
* Returns the "Support" menu item in the left drawer.
* In custom apps, this item displays the application name dynamically.
* Child activities are responsible for defining this drawer item.
*/
abstract val supportDrawerMenuItem: DrawerMenuItem?
/**
* Returns the "About App" menu item in the left drawer.
* For custom apps, this item is shown if configured.
* It is not included in the main Kiwix app.
* Child activities are responsible for defining this drawer item.
*/
abstract val aboutAppDrawerMenuItem: DrawerMenuItem?
val rightNavigationDrawerMenuItems = listOf<DrawerMenuGroup>(
bookRelatedDrawerGroup,
settingDrawerGroup,
helpAndSupportDrawerGroup
)
protected abstract fun getIconResId(): Int
abstract val readerFragmentResId: Int
abstract fun createApplicationShortcuts()

View File

@ -18,10 +18,11 @@
package org.kiwix.kiwixmobile.core.main
import androidx.annotation.DrawableRes
data class DrawerMenuItem(
val id: Int,
val title: String,
val iconRes: Int,
@DrawableRes val iconRes: Int,
val visible: Boolean = true,
val onClick: () -> Unit
)

View File

@ -28,16 +28,18 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import org.kiwix.kiwixmobile.core.R
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH
@Composable
fun MainDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
fun LeftDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
Surface(
modifier = Modifier
.width(NAVIGATION_DRAWER_WIDTH)
@ -47,7 +49,7 @@ fun MainDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
Column {
// Banner image at the top
Image(
painter = painterResource(id = R.drawable.ic_home_kiwix_banner),
painter = IconItem.MipmapImage(R.drawable.ic_home_kiwix_banner).toPainter(),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
@ -60,7 +62,7 @@ fun MainDrawerMenu(drawerMenuGroupList: List<DrawerMenuGroup>) {
}
@Composable
fun DrawerGroup(items: List<DrawerMenuItem>) {
private fun DrawerGroup(items: List<DrawerMenuItem>) {
Column {
items.filter { it.visible }.forEach { item ->
DrawerMenuItemView(item)
@ -69,7 +71,7 @@ fun DrawerGroup(items: List<DrawerMenuItem>) {
}
@Composable
fun DrawerMenuItemView(item: DrawerMenuItem) {
private fun DrawerMenuItemView(item: DrawerMenuItem) {
ListItem(
leadingContent = {
Icon(

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.core.ui.models
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
@ -57,20 +58,19 @@ fun IconItem.toPainter(): Painter {
is IconItem.Drawable -> painterResource(drawableRes)
is IconItem.ImageBitmap -> remember { BitmapPainter(bitmap) }
is IconItem.MipmapImage -> {
val drawable = ContextCompat.getDrawable(LocalContext.current, mipmapResId)
val imageBitmap = when {
drawable is BitmapDrawable -> drawable.bitmap.asImageBitmap()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> {
val bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap.asImageBitmap()
}
else -> {
createBitmap(0, 0).asImageBitmap()
}
val context = LocalContext.current
val drawable = ContextCompat.getDrawable(context, mipmapResId)
val imageBitmap = drawable?.let {
val width = it.intrinsicWidth.takeIf { w -> w > 0 } ?: 100
val height = it.intrinsicHeight.takeIf { h -> h > 0 } ?: 100
val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap)
it.setBounds(0, 0, canvas.width, canvas.height)
it.draw(canvas)
bitmap.asImageBitmap()
} ?: run {
// fallback empty bitmap if drawable is null
createBitmap(1, 1).asImageBitmap()
}
return remember { BitmapPainter(imageBitmap) }
}

View File

@ -30,6 +30,7 @@ import androidx.drawerlayout.widget.DrawerLayout
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.R.drawable
import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets
@ -37,6 +38,7 @@ 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.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.DrawerMenuItem
import org.kiwix.kiwixmobile.core.main.NEW_TAB_SHORTCUT_ID
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.custom.BuildConfig
@ -50,7 +52,7 @@ class CustomMainActivity : CoreMainActivity() {
supportFragmentManager.findFragmentById(
R.id.custom_nav_controller
) as NavHostFragment
)
)
.navController
}
override val drawerContainerLayout: DrawerLayout by lazy {
@ -192,6 +194,73 @@ class CustomMainActivity : CoreMainActivity() {
override fun getIconResId() = R.mipmap.ic_launcher
/**
* 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
*/
override val zimHostDrawerMenuItem: DrawerMenuItem? = null
/**
* 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
*/
override val helpDrawerMenuItem: DrawerMenuItem? = null
override val supportDrawerMenuItem: DrawerMenuItem? =
/**
* 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()) {
DrawerMenuItem(
title = CoreApp.instance.getString(
string.menu_support_kiwix_for_custom_apps,
CoreApp.instance.getString(R.string.app_name)
),
iconRes = drawable.ic_support_24px,
true,
onClick = {
externalLinkOpener.openExternalUrl(BuildConfig.SUPPORT_URL.toUri().browserIntent(), false)
}
)
} else {
/**
* If custom app is not configured to show the "Support app_name" in navigation
* then remove it from navigation.
*/
null
}
/**
* 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.
*/
override val aboutAppDrawerMenuItem: DrawerMenuItem? =
if (BuildConfig.ABOUT_APP_URL.isNotEmpty()) {
DrawerMenuItem(
title = CoreApp.instance.getString(
string.menu_about_app,
CoreApp.instance.getString(R.string.app_name)
),
iconRes = drawable.ic_baseline_info,
true,
onClick = {
externalLinkOpener.openExternalUrl(BuildConfig.SUPPORT_URL.toUri().browserIntent(), false)
}
)
} else {
null
}
override fun createApplicationShortcuts() {
// Remove previously added dynamic shortcuts for old ids if any found.
removeOutdatedIdShortcuts()