diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/BottomNavItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/main/BottomNavItem.kt new file mode 100644 index 000000000..17b77eddf --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/BottomNavItem.kt @@ -0,0 +1,27 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +package org.kiwix.kiwixmobile.main + +import androidx.annotation.IdRes + +data class BottomNavItem( + @IdRes val id: Int, + val title: String, + val iconRes: Int +) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivityScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivityScreen.kt new file mode 100644 index 000000000..2b3dd8131 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivityScreen.kt @@ -0,0 +1,156 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +package org.kiwix.kiwixmobile.main + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomAppBarScrollBehavior +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.FragmentManager +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import org.kiwix.kiwixmobile.R.drawable +import org.kiwix.kiwixmobile.R.id +import org.kiwix.kiwixmobile.core.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomNavigationBar( + navController: NavController, + scrollBehavior: BottomAppBarScrollBehavior, + topLevelDestinations: List +) { + val bottomNavItems = listOf( + BottomNavItem( + id = id.readerFragment, + title = stringResource(id = R.string.reader), + iconRes = drawable.ic_reader_navigation_white_24px + ), + BottomNavItem( + id = id.libraryFragment, + title = stringResource(id = R.string.library), + iconRes = drawable.ic_library_navigation_white_24dp + ), + BottomNavItem( + id = id.downloadsFragment, + title = stringResource(id = R.string.download), + iconRes = drawable.ic_download_navigation_white_24dp + ) + ) + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestinationId = navBackStackEntry?.destination?.id + + if (currentDestinationId in topLevelDestinations) { + BottomAppBar(scrollBehavior = scrollBehavior) { + bottomNavItems.forEach { item -> + NavigationBarItem( + selected = currentDestinationId == item.id, + onClick = { navController.navigate(item.id) }, + icon = { + Icon( + painter = painterResource(id = item.iconRes), + contentDescription = item.title + ) + }, + label = { Text(item.title) } + ) + } + } + } +} + +@Composable +fun MainNavGraph( + fragmentManager: FragmentManager, + navGraphId: Int +) { + val navController = remember { + fragmentManager.findNavController(R.id.nav_host_fragment) + } + + // Drawer states + val leftDrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val rightDrawerVisible = remember { mutableStateOf(false) } + + // Bottom nav destinations + val bottomNavDestinations = listOf( + id.readerFragment, + id.libraryFragment, + id.downloadsFragment + ) + + // Observe current destination + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestinationId = navBackStackEntry?.destination?.id + + // Coroutine scope for drawer + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerState = leftDrawerState, + drawerContent = { DrawerContentLeft() } + ) { + Box(modifier = Modifier.fillMaxSize()) { + // Fragment content + FragmentContainer( + fragmentManager = fragmentManager, + containerId = R.id.nav_host_fragment, + navGraphId = navGraphId + ) + + // Right drawer (slide in) + AnimatedVisibility( + visible = rightDrawerVisible.value, + enter = slideInHorizontally(initialOffsetX = { it }), + exit = slideOutHorizontally(targetOffsetX = { it }), + modifier = Modifier.align(Alignment.CenterEnd) + ) { + DrawerContentRight( + onClose = { rightDrawerVisible.value = false } + ) + } + + // Bottom nav only on selected destinations + if (currentDestinationId in bottomNavDestinations) { + BottomNavigationBar( + navController = navController, + ) + } + } + } +} diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 09f10679e..7a95b3a87 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -372,4 +372,6 @@ object Libs { const val COIL3_COMPOSE = "io.coil-kt.coil3:coil-compose:${Versions.COIL_COMPOSE}" const val COIL3_OKHTTP_COMPOSE = "io.coil-kt.coil3:coil-network-okhttp:${Versions.COIL_COMPOSE}" + const val COMPOSE_NAVIGATION = + "androidx.navigation:navigation-compose:${Versions.COMPOSE_NAVIGATION}" } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index d39d9fd84..eac6b6826 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -117,6 +117,8 @@ object Versions { const val TURBINE_FLOW_TEST = "1.2.0" const val COIL_COMPOSE = "3.2.0" + + const val COMPOSE_NAVIGATION = "2.7.7" } /** diff --git a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt index 4c1458ea7..1b47a47f2 100644 --- a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt +++ b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt @@ -245,6 +245,7 @@ class AllProjectConfigurer { implementation(Libs.COMPOSE_LIVE_DATA) implementation(Libs.COIL3_COMPOSE) implementation(Libs.COIL3_OKHTTP_COMPOSE) + implementation(Libs.COMPOSE_NAVIGATION) // Compose UI test implementation androidTestImplementation(Libs.COMPOSE_UI_TEST_JUNIT) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/DrawerMenuItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/DrawerMenuItem.kt new file mode 100644 index 000000000..8eb9e8c98 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/DrawerMenuItem.kt @@ -0,0 +1,29 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +package org.kiwix.kiwixmobile.core.main + +data class DrawerMenuItem( + val id: Int, + val title: String, + val iconRes: Int, + val visible: Boolean = true, + val onClick: () -> Unit +) + +data class DrawerMenuGroup(val drawerMenuItemList: List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainDrawerMenu.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainDrawerMenu.kt new file mode 100644 index 000000000..bfb79e4c9 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainDrawerMenu.kt @@ -0,0 +1,87 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +package org.kiwix.kiwixmobile.core.main + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +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.utils.ComposeDimens.EIGHT_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NAVIGATION_DRAWER_WIDTH + +@Composable +fun MainDrawerMenu(drawerMenuGroupList: List) { + Surface( + modifier = Modifier + .width(NAVIGATION_DRAWER_WIDTH) + .fillMaxHeight(), + shadowElevation = EIGHT_DP + ) { + Column { + // Banner image at the top + Image( + painter = painterResource(id = R.drawable.ic_home_kiwix_banner), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + drawerMenuGroupList.forEach { + DrawerGroup(it.drawerMenuItemList) + } + } + } +} + +@Composable +fun DrawerGroup(items: List) { + Column { + items.filter { it.visible }.forEach { item -> + DrawerMenuItemView(item) + } + } +} + +@Composable +fun DrawerMenuItemView(item: DrawerMenuItem) { + ListItem( + leadingContent = { + Icon( + painter = painterResource(id = item.iconRes), + contentDescription = item.title + ) + }, + headlineContent = { + Text(text = item.title) + }, + modifier = Modifier + .fillMaxWidth() + .clickable { item.onClick } + ) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 32cd73ee2..569e0fafc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -195,4 +195,7 @@ object ComposeDimens { const val READER_BOTTOM_APP_BAR_DISABLE_BUTTON_ALPHA = 0.38f val SEARCH_PLACEHOLDER_TEXT_SIZE = 12.sp val DONATION_LAYOUT_MAXIMUM_WIDTH = 400.dp + + // MainActivity dimens + val NAVIGATION_DRAWER_WIDTH = 280.dp }