diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt index f51909084..3c1711573 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt @@ -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(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(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(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 = 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) { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryScreen.kt new file mode 100644 index 000000000..13a995d71 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryScreen.kt @@ -0,0 +1,209 @@ +/* + * 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.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, + onRefresh: () -> Unit, + noFilesViewItem: Triple, + onDownloadButtonClick: () -> Unit, + fabButtonClick: () -> Unit, + actionMenuItems: List, + 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, + 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), {}) +// } +// } diff --git a/app/src/main/res/navigation/kiwix_nav_graph.xml b/app/src/main/res/navigation/kiwix_nav_graph.xml index f3f8455d3..fd8717d0b 100644 --- a/app/src/main/res/navigation/kiwix_nav_graph.xml +++ b/app/src/main/res/navigation/kiwix_nav_graph.xml @@ -66,8 +66,7 @@ + android:label="Library">