Migrated the LocalLibraryFragment to jetpack.

* Created `LocalLibraryScreen` for compose UI.
* Refactored the `LocalLibraryFragment` functionality with compose.
* Minor refinement in `KiwixAppBar` for title and hamburger icon.
This commit is contained in:
MohitMaliFtechiz 2025-03-18 18:50:53 +05:30
parent a72d021e95
commit 9be780b863
7 changed files with 361 additions and 64 deletions

View File

@ -41,7 +41,12 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -84,6 +89,9 @@ import org.kiwix.kiwixmobile.core.navigateToAppSettings
import org.kiwix.kiwixmobile.core.navigateToSettings
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.utils.EXTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
@ -116,7 +124,9 @@ import javax.inject.Inject
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"
@Suppress("LargeClass")
class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCallback {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@ -136,8 +146,20 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
private val disposable = CompositeDisposable()
private var fragmentDestinationLibraryBinding: FragmentDestinationLibraryBinding? = null
private var permissionDeniedLayoutShowing = false
private var fileSelectListState: FileSelectListState? = null
private var zimFileUri: Uri? = null
private lateinit var snackBarHostState: SnackbarHostState
private var fileSelectListState = mutableStateOf(FileSelectListState(emptyList()))
/**
* This is a Triple which is responsible for showing and hiding the "No file here",
* and "Download books" button.
*
* A [Triple] containing:
* - [String]: The title text displayed when no files are available.
* - [String]: The label for the download button.
* - [Boolean]: The boolean value for showing or hiding this view.
*/
private var noFilesViewItem = mutableStateOf(Triple("", "", false))
private val zimManageViewModel by lazy {
requireActivity().viewModel<ZimManageViewModel>(viewModelFactory)
@ -154,13 +176,11 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
if (readStorageHasBeenPermanentlyDenied(isGranted)) {
fragmentDestinationLibraryBinding?.apply {
permissionDeniedLayoutShowing = true
fileManagementNoFiles.visibility = VISIBLE
goToDownloadsButtonNoFiles.visibility = VISIBLE
fileManagementNoFiles.text =
requireActivity().resources.getString(string.grant_read_storage_permission)
goToDownloadsButtonNoFiles.text =
requireActivity().resources.getString(string.go_to_settings_label)
zimfilelist.visibility = GONE
noFilesViewItem.value = Triple(
requireActivity().resources.getString(string.grant_read_storage_permission),
requireActivity().resources.getString(string.go_to_settings_label),
true
)
}
} else if (isGranted) {
permissionDeniedLayoutShowing = false
@ -202,25 +222,88 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
): View? {
LanguageUtils(requireActivity())
.changeFont(requireActivity(), sharedPreferenceUtil)
fragmentDestinationLibraryBinding =
FragmentDestinationLibraryBinding.inflate(
inflater,
container,
false
)
val toolbar = fragmentDestinationLibraryBinding?.root?.findViewById<Toolbar>(R.id.toolbar)
val activity = activity as CoreMainActivity
activity.setSupportActionBar(toolbar)
activity.supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(string.library)
}
if (toolbar != null) {
activity.setupDrawerToggle(toolbar)
}
setupMenu()
// fragmentDestinationLibraryBinding =
// FragmentDestinationLibraryBinding.inflate(
// inflater,
// container,
// false
// )
// val toolbar = fragmentDestinationLibraryBinding?.root?.findViewById<Toolbar>(R.id.toolbar)
// val activity = activity as CoreMainActivity
// activity.setSupportActionBar(toolbar)
// activity.supportActionBar?.apply {
// setDisplayHomeAsUpEnabled(true)
// setTitle(string.library)
// }
// if (toolbar != null) {
// activity.setupDrawerToggle(toolbar)
// }
// setupMenu()
return fragmentDestinationLibraryBinding?.root
val composeView = ComposeView(requireContext()).apply {
setContent {
snackBarHostState = remember { SnackbarHostState() }
LocalLibraryScreen(
state = fileSelectListState.value,
snackBarHostState = snackBarHostState,
fabButtonClick = { filePickerButtonClick() },
actionMenuItems = actionMenuItems(),
onClick = { onBookItemClick(it) },
onLongClick = { onBookItemLongClick(it) },
onMultiSelect = { offerAction(RequestSelect(it)) },
onRefresh = {},
swipeRefreshItem = true to true,
noFilesViewItem = noFilesViewItem.value,
onDownloadButtonClick = { downloadBookButtonClick() }
) {
NavigationIcon(
iconItem = IconItem.Vector(Icons.Filled.Menu),
contentDescription = string.open_drawer,
onClick = {
// Manually handle the navigation open/close.
// Since currently we are using the view based navigation drawer in other screens.
// Once we fully migrate to jetpack compose we will refactor this code to use the
// compose navigation.
// TODO Replace with compose based navigation when migration is done.
val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else {
activity.openNavigationDrawer()
}
}
)
}
}
}
return composeView
}
private fun actionMenuItems() = listOf(
ActionMenuItem(
IconItem.Drawable(R.drawable.ic_baseline_mobile_screen_share_24px),
string.get_content_from_nearby_device,
{ navigateToLocalFileTransferFragment() },
isEnabled = true,
testingTag = LOCAL_FILE_TRANSFER_MENU_BUTTON_TESTING_TAG
)
)
private fun onBookItemClick(bookOnDisk: BookOnDisk) {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
showManageExternalStoragePermissionDialog()
} else {
offerAction(RequestNavigateTo(bookOnDisk))
}
}
private fun onBookItemLongClick(bookOnDisk: BookOnDisk) {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
showManageExternalStoragePermissionDialog()
} else {
offerAction(RequestMultiSelection(bookOnDisk))
}
}
private fun setupMenu() {
@ -294,7 +377,6 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
offerAction(FileSelectActions.UserClickedDownloadBooksButton)
}
}
setUpFilePickerButton()
fragmentDestinationLibraryBinding?.zimfilelist?.addOnScrollListener(
SimpleRecyclerViewScrollListener { _, newState ->
@ -314,6 +396,15 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
showCopyMoveDialogForOpenedZimFileFromStorage()
}
private fun downloadBookButtonClick() {
if (permissionDeniedLayoutShowing) {
permissionDeniedLayoutShowing = false
requireActivity().navigateToAppSettings()
} else {
offerAction(FileSelectActions.UserClickedDownloadBooksButton)
}
}
private fun showCopyMoveDialogForOpenedZimFileFromStorage() {
val args = LocalLibraryFragmentArgs.fromBundle(requireArguments())
if (args.zimFileUri.isNotEmpty()) {
@ -374,13 +465,11 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
}
}
private fun setUpFilePickerButton() {
fragmentDestinationLibraryBinding?.selectFile?.setOnClickListener {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
showManageExternalStoragePermissionDialog()
} else if (requestExternalStorageWritePermission()) {
showFileChooser()
}
private fun filePickerButtonClick() {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
showManageExternalStoragePermissionDialog()
} else if (requestExternalStorageWritePermission()) {
showFileChooser()
}
}
@ -546,7 +635,7 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
val effectResult = it.invokeWith(requireActivity() as AppCompatActivity)
if (effectResult is ActionMode) {
actionMode = effectResult
fileSelectListState?.selectedBooks?.size?.let(::setActionModeTitle)
fileSelectListState.value.selectedBooks.size.let(::setActionModeTitle)
}
},
Throwable::printStackTrace
@ -571,30 +660,19 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
}
private fun render(state: FileSelectListState) {
fileSelectListState = state
val items: List<BooksOnDiskListItem> = state.bookOnDiskListItems
bookDelegate.selectionMode = state.selectionMode
booksOnDiskAdapter.items = items
if (items.none(BooksOnDiskListItem::isSelected)) {
fileSelectListState.value = state
if (state.bookOnDiskListItems.none(BooksOnDiskListItem::isSelected)) {
actionMode?.finish()
actionMode = null
}
setActionModeTitle(state.selectedBooks.size)
fragmentDestinationLibraryBinding?.apply {
if (items.isEmpty()) {
fileManagementNoFiles.text = requireActivity().resources.getString(string.no_files_here)
goToDownloadsButtonNoFiles.text =
requireActivity().resources.getString(string.download_books)
fileManagementNoFiles.visibility = View.VISIBLE
goToDownloadsButtonNoFiles.visibility = View.VISIBLE
zimfilelist.visibility = View.GONE
} else {
fileManagementNoFiles.visibility = View.GONE
goToDownloadsButtonNoFiles.visibility = View.GONE
zimfilelist.visibility = View.VISIBLE
}
}
noFilesViewItem.value = Triple(
requireActivity().resources.getString(string.no_files_here),
requireActivity().resources.getString(string.download_books),
// If here are no items available then show the "No files here" text, and "Download books"
// button so that user can go to "Online library" screen by clicking this button.
state.bookOnDiskListItems.isEmpty()
)
}
private fun setActionModeTitle(selectedBookCount: Int) {

View File

@ -0,0 +1,209 @@
/*
* 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.nav.destination.library
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R
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.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FAB_ICON_BOTTOM_MARGIN
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.ui.BookItem
import org.kiwix.kiwixmobile.ui.ZimFilesLanguageHeader
import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming", "LongParameterList", "UnusedParameter")
@Composable
fun LocalLibraryScreen(
state: FileSelectListState,
snackBarHostState: SnackbarHostState,
swipeRefreshItem: Pair<Boolean, Boolean>,
onRefresh: () -> Unit,
noFilesViewItem: Triple<String, String, Boolean>,
onDownloadButtonClick: () -> Unit,
fabButtonClick: () -> Unit,
actionMenuItems: List<ActionMenuItem>,
onClick: ((BookOnDisk) -> Unit)? = null,
onLongClick: ((BookOnDisk) -> Unit)? = null,
onMultiSelect: ((BookOnDisk) -> Unit)? = null,
navigationIcon: @Composable () -> Unit
) {
// val swipeRefreshState = rememberPullToRefreshState()
KiwixTheme {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = snackBarHostState) },
topBar = { KiwixAppBar(R.string.library, navigationIcon, actionMenuItems) },
modifier = Modifier.systemBarsPadding()
) { contentPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
// .pullToRefresh(
// isRefreshing = swipeRefreshItem.first,
// state = swipeRefreshState,
// enabled = swipeRefreshItem.second,
// onRefresh = { onRefresh }
// )
// .pullToRefreshIndicator(
// state = swipeRefreshState,
// isRefreshing = swipeRefreshItem.first
// )
) {
if (noFilesViewItem.third) {
NoFilesView(noFilesViewItem, onDownloadButtonClick)
} else {
BookItemList(state)
}
SelectFileButton(
fabButtonClick,
Modifier
.align(Alignment.BottomEnd)
.padding(end = SIXTEEN_DP, bottom = FAB_ICON_BOTTOM_MARGIN)
)
}
}
}
}
@Composable
private fun BookItemList(
state: FileSelectListState,
onClick: ((BookOnDisk) -> Unit)? = null,
onLongClick: ((BookOnDisk) -> Unit)? = null,
onMultiSelect: ((BookOnDisk) -> Unit)? = null,
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
itemsIndexed(state.bookOnDiskListItems) { index, bookItem ->
when (bookItem) {
is BooksOnDiskListItem.LanguageItem -> {
ZimFilesLanguageHeader(bookItem)
}
is BookOnDisk -> {
BookItem(
index = index,
bookOnDisk = bookItem,
selectionMode = state.selectionMode,
onClick = onClick,
onLongClick = onLongClick,
onMultiSelect = onMultiSelect
)
}
}
}
}
}
@Composable
private fun SelectFileButton(fabButtonClick: () -> Unit, modifier: Modifier) {
FloatingActionButton(
onClick = fabButtonClick,
modifier = modifier,
containerColor = Black,
shape = MaterialTheme.shapes.extraLarge
) {
Icon(
painter = painterResource(id = R.drawable.ic_add_blue_24dp),
contentDescription = stringResource(id = string.select_zim_file),
tint = White
)
}
}
@Composable
fun NoFilesView(
noFilesViewItem: Triple<String, String, Boolean>,
onDownloadButtonClick: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = noFilesViewItem.first,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Medium)
)
Spacer(modifier = Modifier.height(EIGHT_DP))
KiwixButton(noFilesViewItem.second, onDownloadButtonClick)
}
}
// @Preview
// @Preview(name = "NightMode", uiMode = Configuration.UI_MODE_NIGHT_YES)
// @Composable
// fun PreviewLocalLibrary() {
// LocalLibraryScreen(
// state = FileSelectListState(listOf(), SelectionMode.NORMAL),
// snackBarHostState = SnackbarHostState(),
// actionMenuItems = listOf(
// ActionMenuItem(
// IconItem.Drawable(org.kiwix.kiwixmobile.R.drawable.ic_baseline_mobile_screen_share_24px),
// R.string.get_content_from_nearby_device,
// { },
// isEnabled = true,
// testingTag = DELETE_MENU_BUTTON_TESTING_TAG
// )
// ),
// onRefresh = {},
// fabButtonClick = {},
// swipeRefreshItem = false to true,
// noFilesViewItem = Triple(
// stringResource(R.string.no_files_here),
// stringResource(R.string.download_books),
// {}
// )
// ) {
// NavigationIcon(IconItem.Vector(Icons.Filled.Menu), {})
// }
// }

View File

@ -66,8 +66,7 @@
<fragment
android:id="@+id/libraryFragment"
android:name="org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragment"
android:label="Library"
tools:layout="@layout/fragment_destination_library">
android:label="Library">
<action
android:id="@+id/action_navigation_library_to_navigation_reader"
app:destination="@id/readerFragment"

View File

@ -326,13 +326,17 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
handleDrawerOnNavigation()
}
private fun navigationDrawerIsOpen(): Boolean =
fun navigationDrawerIsOpen(): Boolean =
drawerContainerLayout.isDrawerOpen(drawerNavView)
fun closeNavigationDrawer() {
drawerContainerLayout.closeDrawer(drawerNavView)
}
fun openNavigationDrawer() {
drawerContainerLayout.openDrawer(drawerNavView)
}
fun openSupportKiwixExternalLink() {
externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent(), false)
}

View File

@ -36,7 +36,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight.Companion.SemiBold
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black
@ -45,6 +44,7 @@ import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_APP_BAR_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
@ -64,6 +64,7 @@ fun KiwixAppBar(
.background(color = Black),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(Modifier.padding(start = TWO_DP))
navigationIcon()
searchBar?.let {
// Display the search bar when provided
@ -90,7 +91,7 @@ private fun AppBarTitle(
Text(
text = stringResource(titleId),
color = appBarTitleColor,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = SemiBold),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.padding(horizontal = SIXTEEN_DP)
.testTag(TOOLBAR_TITLE_TESTING_TAG)

View File

@ -46,7 +46,10 @@ val KiwixTypography = Typography(
headlineMedium = TextStyle(fontSize = MEDIUM_HEADLINE_TEXT_SIZE),
headlineSmall = TextStyle(fontSize = SMALL_HEADLINE_TEXT_SIZE),
titleLarge = TextStyle(fontSize = LARGE_TITLE_TEXT_SIZE),
titleMedium = TextStyle(fontSize = MEDIUM_TITLE_TEXT_SIZE),
titleMedium = TextStyle(
fontSize = MEDIUM_TITLE_TEXT_SIZE,
fontWeight = FontWeight.Medium,
),
titleSmall = TextStyle(fontSize = SMALL_TITLE_TEXT_SIZE),
bodyLarge = TextStyle(fontSize = LARGE_BODY_TEXT_SIZE),
bodyMedium = TextStyle(

View File

@ -72,7 +72,7 @@ object ComposeDimens {
val MEDIUM_HEADLINE_TEXT_SIZE = 28.sp
val SMALL_HEADLINE_TEXT_SIZE = 24.sp
val LARGE_TITLE_TEXT_SIZE = 22.sp
val MEDIUM_TITLE_TEXT_SIZE = 18.sp
val MEDIUM_TITLE_TEXT_SIZE = 20.sp
val SMALL_TITLE_TEXT_SIZE = 16.sp
val LARGE_BODY_TEXT_SIZE = 16.sp
val MEDIUM_BODY_TEXT_SIZE = 14.sp
@ -93,6 +93,9 @@ object ComposeDimens {
val MAXIMUM_HEIGHT_OF_QR_CODE = 128.dp
val MINIMUM_HEIGHT_OF_BOOKS_LIST = 256.dp
// BookItem dimes
// BookItem dimens
val BOOK_ICON_SIZE = 40.dp
// LocalLibraryFragment dimens
val FAB_ICON_BOTTOM_MARGIN = 66.dp
}