From 93dc61e113669312e385b73e92927258ec24b2f3 Mon Sep 17 00:00:00 2001 From: Rifat Azad Date: Sat, 28 Jun 2025 19:45:32 +0600 Subject: [PATCH] manager: change FAB visibility with scrolling gestures on module screen list --- .../com/rifsxd/ksunext/ui/screen/Module.kt | 121 ++++++++++++------ 1 file changed, 83 insertions(+), 38 deletions(-) diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt index 97f19736..c3410cfc 100644 --- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt @@ -77,6 +77,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.core.tween import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -164,6 +170,30 @@ fun ModuleScreen(navigator: DestinationsNavigator) { contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } + val listState = rememberLazyListState() + var showFab by remember { mutableStateOf(true) } + + LaunchedEffect(listState) { + var lastIndex = listState.firstVisibleItemIndex + var lastOffset = listState.firstVisibleItemScrollOffset + + snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } + .collect { (currIndex, currOffset) -> + val isScrollingDown = currIndex > lastIndex || + (currIndex == lastIndex && currOffset > lastOffset + 4) + val isScrollingUp = currIndex < lastIndex || + (currIndex == lastIndex && currOffset < lastOffset - 4) + + when { + isScrollingDown && showFab -> showFab = false + isScrollingUp && !showFab -> showFab = true + } + + lastIndex = currIndex + lastOffset = currOffset + } + } + Scaffold( topBar = { SearchAppBar( @@ -289,46 +319,58 @@ fun ModuleScreen(navigator: DestinationsNavigator) { }, floatingActionButton = { if (!hideInstallButton) { - val moduleInstall = stringResource(id = R.string.module_install) - val selectZipLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode != RESULT_OK) { - return@rememberLauncherForActivityResult - } - val data = result.data ?: return@rememberLauncherForActivityResult - val clipData = data.clipData - - val uris = mutableListOf() - if (clipData != null) { - for (i in 0 until clipData.itemCount) { - clipData.getItemAt(i)?.uri?.let { uris.add(it) } + AnimatedVisibility( + visible = showFab, + enter = scaleIn( + animationSpec = tween(200), + initialScale = 0.8f + ) + fadeIn(animationSpec = tween(400)), + exit = scaleOut( + animationSpec = tween(200), + targetScale = 0.8f + ) + fadeOut(animationSpec = tween(400)) + ) { + val moduleInstall = stringResource(id = R.string.module_install) + val selectZipLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode != RESULT_OK) { + return@rememberLauncherForActivityResult } - } else { - data.data?.let { uris.add(it) } + val data = result.data ?: return@rememberLauncherForActivityResult + val clipData = data.clipData + + val uris = mutableListOf() + if (clipData != null) { + for (i in 0 until clipData.itemCount) { + clipData.getItemAt(i)?.uri?.let { uris.add(it) } + } + } else { + data.data?.let { uris.add(it) } + } + + if (uris.isEmpty()) return@rememberLauncherForActivityResult + + viewModel.updateZipUris(uris) + + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(uris))) + viewModel.clearZipUris() + viewModel.markNeedRefresh() } - if (uris.isEmpty()) return@rememberLauncherForActivityResult - - viewModel.updateZipUris(uris) - - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(uris))) - viewModel.clearZipUris() - viewModel.markNeedRefresh() + ExtendedFloatingActionButton( + onClick = { + // Select the zip files to install + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/zip" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + selectZipLauncher.launch(intent) + }, + icon = { Icon(Icons.Filled.Add, moduleInstall) }, + text = { Text(text = moduleInstall) }, + ) } - - ExtendedFloatingActionButton( - onClick = { - // Select the zip files to install - val intent = Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/zip" - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - } - selectZipLauncher.launch(intent) - }, - icon = { Icon(Icons.Filled.Add, moduleInstall) }, - text = { Text(text = moduleInstall) }, - ) } }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), @@ -370,7 +412,8 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } }, context = context, - snackBarHost = snackBarHost + snackBarHost = snackBarHost, + listState = listState ) } } @@ -388,6 +431,7 @@ private fun ModuleList( onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, context: Context, snackBarHost: SnackbarHostState, + listState: LazyListState ) { val failedEnable = stringResource(R.string.module_failed_to_enable) val failedDisable = stringResource(R.string.module_failed_to_disable) @@ -560,6 +604,7 @@ private fun ModuleList( isRefreshing = viewModel.isRefreshing ) { LazyColumn( + state = listState, modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = remember { @@ -567,7 +612,7 @@ private fun ModuleList( start = 16.dp, top = 16.dp, end = 16.dp, - bottom = 16.dp + 56.dp + 16.dp + 48.dp + 6.dp /* Scaffold Fab Spacing + Fab container height + SnackBar height */ + bottom = 16.dp ) }, ) {