Created LocalLibraryScreenState to encapsulate all UI-related states, reducing state management's complexity.

This commit is contained in:
MohitMaliFtechiz 2025-03-19 17:33:35 +05:30
parent 5b507d8f3d
commit 80fbf74d9a
3 changed files with 193 additions and 144 deletions

View File

@ -40,9 +40,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
@ -87,9 +85,6 @@ import org.kiwix.kiwixmobile.core.utils.EXTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_DOWN
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_UP
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
@ -97,7 +92,6 @@ import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.isSplittedZimFile import org.kiwix.kiwixmobile.core.utils.files.FileUtils.isSplittedZimFile
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem 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.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.databinding.FragmentDestinationLibraryBinding
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.nav.destination.library.CopyMoveFileHandler import org.kiwix.kiwixmobile.nav.destination.library.CopyMoveFileHandler
import org.kiwix.kiwixmobile.zimManager.MAX_PROGRESS import org.kiwix.kiwixmobile.zimManager.MAX_PROGRESS
@ -134,34 +128,19 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val disposable = CompositeDisposable() private val disposable = CompositeDisposable()
private var fragmentDestinationLibraryBinding: FragmentDestinationLibraryBinding? = null
private var permissionDeniedLayoutShowing = false private var permissionDeniedLayoutShowing = false
private var zimFileUri: Uri? = null private var zimFileUri: Uri? = null
private lateinit var snackBarHostState: SnackbarHostState val libraryScreenState = mutableStateOf(
private var fileSelectListState = mutableStateOf(FileSelectListState(emptyList())) LocalLibraryScreenState(
fileSelectListState = FileSelectListState(emptyList()),
/** snackBarHostState = SnackbarHostState(),
* This is a Triple which is responsible for showing and hiding the "No file here", swipeRefreshItem = Pair(false, true),
* and "Download books" button. scanningProgressItem = Pair(false, ZERO),
* noFilesViewItem = Triple("", "", false),
* A [Triple] containing: actionMenuItems = listOf(),
* - [String]: The title text displayed when no files are available. bottomNavigationHeight = ZERO
* - [String]: The label for the download button. )
* - [Boolean]: The boolean value for showing or hiding this view. )
*/
private var noFilesViewItem = mutableStateOf(Triple("", "", false))
/**
* This is a Pair which is responsible for showing and hiding the "Pull to refresh"
* animation.
*
* A [Pair] containing:
* - [Boolean]: The first boolean triggers/hide the "pull to refresh" animation.
* - [Boolean]: The second boolean enable/disable the "pull to refresh".
*/
private var swipeRefreshItem = mutableStateOf(Pair(false, true))
private var scanningProgressItem = mutableStateOf(Pair(false, ZERO))
private val zimManageViewModel by lazy { private val zimManageViewModel by lazy {
requireActivity().viewModel<ZimManageViewModel>(viewModelFactory) requireActivity().viewModel<ZimManageViewModel>(viewModelFactory)
@ -174,10 +153,12 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
val isGranted = permissionResult.values.all { it } val isGranted = permissionResult.values.all { it }
val isPermanentlyDenied = readStorageHasBeenPermanentlyDenied(isGranted) val isPermanentlyDenied = readStorageHasBeenPermanentlyDenied(isGranted)
permissionDeniedLayoutShowing = isPermanentlyDenied permissionDeniedLayoutShowing = isPermanentlyDenied
noFilesViewItem.value = Triple( updateLibraryScreenState(
requireActivity().resources.getString(string.grant_read_storage_permission), noFilesViewItem = Triple(
requireActivity().resources.getString(string.go_to_settings_label), requireActivity().resources.getString(string.grant_read_storage_permission),
isPermanentlyDenied requireActivity().resources.getString(string.go_to_settings_label),
isPermanentlyDenied
)
) )
} }
@ -195,20 +176,18 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
val composeView = ComposeView(requireContext()).apply { val composeView = ComposeView(requireContext()).apply {
setContent { setContent {
snackBarHostState = remember { SnackbarHostState() } updateLibraryScreenState(
bottomNavigationHeight = getBottomNavigationHeight(),
actionMenuItems = actionMenuItems()
)
LocalLibraryScreen( LocalLibraryScreen(
state = fileSelectListState.value, state = libraryScreenState.value,
snackBarHostState = snackBarHostState,
fabButtonClick = { filePickerButtonClick() }, fabButtonClick = { filePickerButtonClick() },
actionMenuItems = actionMenuItems(),
onClick = { onBookItemClick(it) }, onClick = { onBookItemClick(it) },
onLongClick = { onBookItemLongClick(it) }, onLongClick = { onBookItemLongClick(it) },
onMultiSelect = { offerAction(RequestSelect(it)) }, onMultiSelect = { offerAction(RequestSelect(it)) },
onRefresh = { onSwipeRefresh() }, onRefresh = { onSwipeRefresh() },
swipeRefreshItem = swipeRefreshItem.value,
noFilesViewItem = noFilesViewItem.value,
onDownloadButtonClick = { downloadBookButtonClick() }, onDownloadButtonClick = { downloadBookButtonClick() },
scanningProgressItem = scanningProgressItem.value
) { ) {
NavigationIcon( NavigationIcon(
iconItem = IconItem.Vector(Icons.Filled.Menu), iconItem = IconItem.Vector(Icons.Filled.Menu),
@ -260,6 +239,28 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
} }
} }
private fun updateLibraryScreenState(
fileSelectListState: FileSelectListState? = null,
snackBarHostState: SnackbarHostState? = null,
swipeRefreshItem: Pair<Boolean, Boolean>? = null,
scanningProgressItem: Pair<Boolean, Int>? = null,
noFilesViewItem: Triple<String, String, Boolean>? = null,
actionMenuItems: List<ActionMenuItem>? = null,
bottomNavigationHeight: Int? = null
) {
libraryScreenState.value = libraryScreenState.value.copy(
fileSelectListState = fileSelectListState ?: libraryScreenState.value.fileSelectListState,
snackBarHostState = snackBarHostState ?: libraryScreenState.value.snackBarHostState,
swipeRefreshItem = swipeRefreshItem ?: libraryScreenState.value.swipeRefreshItem,
scanningProgressItem = scanningProgressItem
?: libraryScreenState.value.scanningProgressItem,
noFilesViewItem = noFilesViewItem ?: libraryScreenState.value.noFilesViewItem,
actionMenuItems = actionMenuItems ?: libraryScreenState.value.actionMenuItems,
bottomNavigationHeight = bottomNavigationHeight
?: libraryScreenState.value.bottomNavigationHeight
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
copyMoveFileHandler?.apply { copyMoveFileHandler?.apply {
@ -270,38 +271,20 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
.also { .also {
coreMainActivity.navHostContainer coreMainActivity.navHostContainer
.setBottomMarginToFragmentContainerView(0) .setBottomMarginToFragmentContainerView(0)
getBottomNavigationView()?.let { bottomNavigationView ->
setBottomMarginToSwipeRefreshLayout(bottomNavigationView.measuredHeight)
}
} }
disposable.add(sideEffects()) disposable.add(sideEffects())
disposable.add(fileSelectActions()) disposable.add(fileSelectActions())
zimManageViewModel.deviceListScanningProgress.observe(viewLifecycleOwner) { zimManageViewModel.deviceListScanningProgress.observe(viewLifecycleOwner) {
// hide this progress bar when scanning is complete. updateLibraryScreenState(
scanningProgressItem.value = Pair(it != MAX_PROGRESS, it) // hide this progress bar when scanning is complete.
// enable if the previous scanning is completes. scanningProgressItem = Pair(it != MAX_PROGRESS, it),
swipeRefreshItem.value = Pair(false, it == MAX_PROGRESS) // enable if the previous scanning is completes.
swipeRefreshItem = Pair(false, it == MAX_PROGRESS)
)
} }
if (savedInstanceState != null && savedInstanceState.getBoolean(WAS_IN_ACTION_MODE)) { if (savedInstanceState != null && savedInstanceState.getBoolean(WAS_IN_ACTION_MODE)) {
zimManageViewModel.fileSelectActions.offer(FileSelectActions.RestartActionMode) zimManageViewModel.fileSelectActions.offer(FileSelectActions.RestartActionMode)
} }
fragmentDestinationLibraryBinding?.zimfilelist?.addOnScrollListener(
SimpleRecyclerViewScrollListener { _, newState ->
when (newState) {
SCROLL_DOWN -> {
setBottomMarginToSwipeRefreshLayout(0)
}
SCROLL_UP -> {
getBottomNavigationView()?.let {
setBottomMarginToSwipeRefreshLayout(it.measuredHeight)
}
}
}
}
)
showCopyMoveDialogForOpenedZimFileFromStorage() showCopyMoveDialogForOpenedZimFileFromStorage()
} }
@ -325,22 +308,24 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
private fun onSwipeRefresh() { private fun onSwipeRefresh() {
if (permissionDeniedLayoutShowing) { if (permissionDeniedLayoutShowing) {
// When permission denied layout is showing hide the "Swipe refresh". // When permission denied layout is showing hide the "Swipe refresh".
swipeRefreshItem.value = false to true updateLibraryScreenState(swipeRefreshItem = false to true)
} else { } else {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) { if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
showManageExternalStoragePermissionDialog() showManageExternalStoragePermissionDialog()
// Set loading to false since the dialog is currently being displayed. // Set loading to false since the dialog is currently being displayed.
// If the user clicks on "No" in the permission dialog, // If the user clicks on "No" in the permission dialog,
// the loading icon remains visible infinitely. // the loading icon remains visible infinitely.
swipeRefreshItem.value = false to true updateLibraryScreenState(swipeRefreshItem = false to true)
} else { } else {
// hide the swipe refreshing because now we are showing the ContentLoadingProgressBar // hide the swipe refreshing because now we are showing the ContentLoadingProgressBar
// to show the progress of how many files are scanned. // to show the progress of how many files are scanned.
// disable the swipe refresh layout until the ongoing scanning will not complete // disable the swipe refresh layout until the ongoing scanning will not complete
// to avoid multiple scanning. // to avoid multiple scanning.
swipeRefreshItem.value = false to false updateLibraryScreenState(
// Show the progress Bar. swipeRefreshItem = false to false,
scanningProgressItem.value = true to ZERO // Show the progress Bar.
scanningProgressItem = true to ZERO
)
requestFileSystemCheck() requestFileSystemCheck()
} }
} }
@ -360,13 +345,7 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
private fun getBottomNavigationView() = private fun getBottomNavigationView() =
requireActivity().findViewById<BottomNavigationView>(R.id.bottom_nav_view) requireActivity().findViewById<BottomNavigationView>(R.id.bottom_nav_view)
private fun setBottomMarginToSwipeRefreshLayout(marginBottom: Int) { private fun getBottomNavigationHeight() = getBottomNavigationView().measuredHeight
fragmentDestinationLibraryBinding?.zimSwiperefresh?.apply {
val params = layoutParams as CoordinatorLayout.LayoutParams?
params?.bottomMargin = marginBottom
requestLayout()
}
}
private fun filePickerButtonClick() { private fun filePickerButtonClick() {
if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) { if (!requireActivity().isManageExternalStoragePermissionGranted(sharedPreferenceUtil)) {
@ -510,10 +489,12 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
) { ) {
checkPermissions() checkPermissions()
} else if (!permissionDeniedLayoutShowing) { } else if (!permissionDeniedLayoutShowing) {
noFilesViewItem.value = Triple( updateLibraryScreenState(
requireActivity().resources.getString(string.no_files_here), noFilesViewItem = Triple(
requireActivity().resources.getString(string.download_books), requireActivity().resources.getString(string.no_files_here),
false requireActivity().resources.getString(string.download_books),
false
)
) )
} }
} }
@ -540,7 +521,11 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
val effectResult = it.invokeWith(requireActivity() as AppCompatActivity) val effectResult = it.invokeWith(requireActivity() as AppCompatActivity)
if (effectResult is ActionMode) { if (effectResult is ActionMode) {
actionMode = effectResult actionMode = effectResult
fileSelectListState.value.selectedBooks.size.let(::setActionModeTitle) libraryScreenState
.value
.fileSelectListState
.selectedBooks
.size.let(::setActionModeTitle)
} }
}, },
Throwable::printStackTrace Throwable::printStackTrace
@ -568,20 +553,21 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
// Force recomposition by first setting an empty list before assigning the updated list. // Force recomposition by first setting an empty list before assigning the updated list.
// This is necessary because modifying an object's property doesn't trigger recomposition, // This is necessary because modifying an object's property doesn't trigger recomposition,
// as Compose still considers the list unchanged. // as Compose still considers the list unchanged.
fileSelectListState.value = FileSelectListState(emptyList()) updateLibraryScreenState(
fileSelectListState.value = state fileSelectListState = state,
noFilesViewItem = 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()
)
)
if (state.bookOnDiskListItems.none(BooksOnDiskListItem::isSelected)) { if (state.bookOnDiskListItems.none(BooksOnDiskListItem::isSelected)) {
actionMode?.finish() actionMode?.finish()
actionMode = null actionMode = null
} }
setActionModeTitle(state.selectedBooks.size) setActionModeTitle(state.selectedBooks.size)
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) { private fun setActionModeTitle(selectedBookCount: Int) {
@ -679,7 +665,7 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
} }
private fun showStorageSelectionSnackBar(message: String) { private fun showStorageSelectionSnackBar(message: String) {
snackBarHostState.snack( libraryScreenState.value.snackBarHostState.snack(
message = message, message = message,
actionLabel = getString(string.download_change_storage), actionLabel = getString(string.download_change_storage),
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,

View File

@ -26,35 +26,42 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.R.string import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.components.SwipeRefreshLayout import org.kiwix.kiwixmobile.core.ui.components.SwipeRefreshLayout
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP 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.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TEN_DP
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem 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.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.ui.BookItem import org.kiwix.kiwixmobile.ui.BookItem
@ -62,54 +69,65 @@ import org.kiwix.kiwixmobile.ui.ZimFilesLanguageHeader
import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming", "LongParameterList") @Suppress("ComposableLambdaParameterNaming")
@Composable @Composable
fun LocalLibraryScreen( fun LocalLibraryScreen(
state: FileSelectListState, state: LocalLibraryScreenState,
snackBarHostState: SnackbarHostState,
swipeRefreshItem: Pair<Boolean, Boolean>,
onRefresh: () -> Unit, onRefresh: () -> Unit,
scanningProgressItem: Pair<Boolean, Int>,
noFilesViewItem: Triple<String, String, Boolean>,
onDownloadButtonClick: () -> Unit, onDownloadButtonClick: () -> Unit,
fabButtonClick: () -> Unit, fabButtonClick: () -> Unit,
actionMenuItems: List<ActionMenuItem>,
onClick: ((BookOnDisk) -> Unit)? = null, onClick: ((BookOnDisk) -> Unit)? = null,
onLongClick: ((BookOnDisk) -> Unit)? = null, onLongClick: ((BookOnDisk) -> Unit)? = null,
onMultiSelect: ((BookOnDisk) -> Unit)? = null, onMultiSelect: ((BookOnDisk) -> Unit)? = null,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val lazyListState = rememberLazyListState()
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) }
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.collect { scrollOffset ->
bottomNavHeight.value = if (scrollOffset > 0) ZERO.dp else bottomNavHeightInDp
}
}
KiwixTheme { KiwixTheme {
Scaffold( Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = snackBarHostState) }, snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = { KiwixAppBar(R.string.library, navigationIcon, actionMenuItems) }, topBar = { KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems) },
modifier = Modifier.systemBarsPadding() modifier = Modifier.systemBarsPadding()
) { contentPadding -> ) { contentPadding ->
SwipeRefreshLayout( SwipeRefreshLayout(
isRefreshing = swipeRefreshItem.first, isRefreshing = state.swipeRefreshItem.first,
isEnabled = swipeRefreshItem.second, isEnabled = state.swipeRefreshItem.second,
onRefresh = onRefresh, onRefresh = onRefresh,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(contentPadding) .padding(contentPadding)
.padding(bottom = bottomNavHeight.value)
) { ) {
if (scanningProgressItem.first) { if (state.scanningProgressItem.first) {
ContentLoadingProgressBar( ContentLoadingProgressBar(
progressBarStyle = ProgressBarStyle.HORIZONTAL, progressBarStyle = ProgressBarStyle.HORIZONTAL,
progress = scanningProgressItem.second progress = state.scanningProgressItem.second
) )
} }
if (noFilesViewItem.third) { if (state.noFilesViewItem.third) {
NoFilesView(noFilesViewItem, onDownloadButtonClick) NoFilesView(state.noFilesViewItem, onDownloadButtonClick)
} else { } else {
BookItemList(state, onClick, onLongClick, onMultiSelect) BookItemList(
state.fileSelectListState,
onClick,
onLongClick,
onMultiSelect,
lazyListState
)
} }
SelectFileButton( SelectFileButton(
fabButtonClick, fabButtonClick,
Modifier Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.padding(end = SIXTEEN_DP, bottom = FAB_ICON_BOTTOM_MARGIN) .padding(end = SIXTEEN_DP, bottom = TEN_DP)
) )
} }
} }
@ -122,8 +140,9 @@ private fun BookItemList(
onClick: ((BookOnDisk) -> Unit)? = null, onClick: ((BookOnDisk) -> Unit)? = null,
onLongClick: ((BookOnDisk) -> Unit)? = null, onLongClick: ((BookOnDisk) -> Unit)? = null,
onMultiSelect: ((BookOnDisk) -> Unit)? = null, onMultiSelect: ((BookOnDisk) -> Unit)? = null,
lazyListState: LazyListState,
) { ) {
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize(), state = lazyListState) {
itemsIndexed(state.bookOnDiskListItems) { index, bookItem -> itemsIndexed(state.bookOnDiskListItems) { index, bookItem ->
when (bookItem) { when (bookItem) {
is BooksOnDiskListItem.LanguageItem -> { is BooksOnDiskListItem.LanguageItem -> {
@ -179,32 +198,3 @@ fun NoFilesView(
KiwixButton(noFilesViewItem.second, onDownloadButtonClick) 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

@ -0,0 +1,73 @@
/*
* 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.local
import androidx.compose.material3.SnackbarHostState
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState
/**
* Represents the UI state for the Local Library Screen.
*
* This data class encapsulates all UI-related states in a single object,
* reducing complexity in the Fragment.
*/
data class LocalLibraryScreenState(
/**
* Manages the file selection list state.
*/
val fileSelectListState: FileSelectListState,
/**
* Handles snack bar messages and displays.
*/
val snackBarHostState: SnackbarHostState,
/**
* Controls the visibility and behavior of the "Pull to refresh" animation.
*
* A [Pair] containing:
* - [Boolean]: The first boolean triggers/hides the "pull to refresh" animation.
* - [Boolean]: The second boolean enables/disables the "pull to refresh" gesture.
*/
val swipeRefreshItem: Pair<Boolean, Boolean>,
/**
* Represents the scanning progress state.
*
* A [Pair] containing:
* - [Boolean]: Whether scanning is in progress.
* - [Int]: The progress percentage of the scan.
*/
val scanningProgressItem: Pair<Boolean, Int>,
/**
* Controls the visibility of the "No files here" message and the "Download books" button.
*
* A [Triple] containing:
* - [String]: The title text displayed when no files are available.
* - [String]: The label for the download button.
* - [Boolean]: Whether to show or hide this view.
*/
val noFilesViewItem: Triple<String, String, Boolean>,
/**
* Represents a list of action menu items available in the screen's top app bar.
*/
val actionMenuItems: List<ActionMenuItem>,
/**
* Stores the height of the bottom navigation bar in pixels.
*/
val bottomNavigationHeight: Int
)