From 27c6186e9ff62f4b6e4be23237f39a869620c284 Mon Sep 17 00:00:00 2001 From: Jing <42014615+jing332@users.noreply.github.com> Date: Thu, 1 Feb 2024 22:04:55 +0800 Subject: [PATCH] refactor: check --- .../compose_filepicker/MainActivity.kt | 55 +++++++++- .../com/github/jing332/filepicker/Contants.kt | 2 +- .../github/jing332/filepicker/FilePicker.kt | 65 ++++++++---- .../jing332/filepicker/FilePickerConfig.kt | 4 +- .../jing332/filepicker/FilePickerViewModel.kt | 6 ++ .../filepicker/{ => listpage}/FileListPage.kt | 59 ++++++----- .../filepicker/listpage/FileListPageState.kt | 100 ++++++++++++++++++ .../{ => listpage}/FileListPageViewModel.kt | 58 ++-------- 8 files changed, 244 insertions(+), 105 deletions(-) rename filepicker/src/main/java/com/github/jing332/filepicker/{ => listpage}/FileListPage.kt (78%) create mode 100644 filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageState.kt rename filepicker/src/main/java/com/github/jing332/filepicker/{ => listpage}/FileListPageViewModel.kt (59%) diff --git a/app/src/main/java/com/github/jing332/compose_filepicker/MainActivity.kt b/app/src/main/java/com/github/jing332/compose_filepicker/MainActivity.kt index b4afe00..8376211 100644 --- a/app/src/main/java/com/github/jing332/compose_filepicker/MainActivity.kt +++ b/app/src/main/java/com/github/jing332/compose_filepicker/MainActivity.kt @@ -1,29 +1,74 @@ package com.github.jing332.compose_filepicker import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.github.jing332.compose_filepicker.ui.theme.ComposefilepickerTheme import com.github.jing332.filepicker.FilePicker import com.github.jing332.filepicker.FilePickerConfig +import com.github.jing332.filepicker.model.IFileModel class MainActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { ComposefilepickerTheme { - Scaffold { - Column(Modifier.padding(bottom = it.calculateBottomPadding())) { - FilePicker(config = FilePickerConfig( + var showSelectedList by remember { mutableStateOf?>(null) } + if (showSelectedList != null) { + val list = showSelectedList!! + ModalBottomSheet(onDismissRequest = { showSelectedList = null }) { + LazyColumn { + items(list) { + Column(Modifier.padding(16.dp)) { + Text(text = it.name) + Text(text = it.path) + } + } + } + } + } + + Scaffold { paddingValues -> + Column(Modifier.padding(bottom = paddingValues.calculateBottomPadding())) { + FilePicker( + config = FilePickerConfig( // fileFilter = { it.name.startsWith("a", ignoreCase = true) }, - fileSelector = { it.name.startsWith("A") } - )) + + // Only checkable name prefix is 'A' files + fileSelector = { _, checked -> + checked.name.startsWith("A").also { + if (!it) { + Toast.makeText( + this@MainActivity, + "Only items starting with 'A' can be checked", + Toast.LENGTH_SHORT + ).show() + } + } + } + ), + onConfirmSelect = { + showSelectedList = it + } + ) } } } diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/Contants.kt b/filepicker/src/main/java/com/github/jing332/filepicker/Contants.kt index 438a691..50a0587 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/Contants.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/Contants.kt @@ -8,7 +8,7 @@ val LocalFilePickerConfig = compositionLocalOf { error("No con object Contants { const val ROUTE_PAGE = "page" - const val ARG_URI = "uri" + const val ARG_PATH = "uri" const val DEFAULT_ROOT_PATH = "/storage/emulated/0" } \ No newline at end of file diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/FilePicker.kt b/filepicker/src/main/java/com/github/jing332/filepicker/FilePicker.kt index 1d2c184..7d89256 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/FilePicker.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/FilePicker.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -28,8 +27,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.github.jing332.filepicker.Contants.ARG_URI +import com.github.jing332.filepicker.Contants.ARG_PATH import com.github.jing332.filepicker.Contants.ROUTE_PAGE +import com.github.jing332.filepicker.listpage.FileListPage +import com.github.jing332.filepicker.listpage.FileListPageState +import com.github.jing332.filepicker.model.IFileModel import com.github.jing332.filepicker.model.NormalFile import com.github.jing332.filepicker.utils.navigate import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -75,7 +77,11 @@ private fun PermissionGrant() { @Composable -fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePickerConfig()) { +fun FilePicker( + modifier: Modifier = Modifier, + config: FilePickerConfig = FilePickerConfig(), + onConfirmSelect: (List) -> Unit +) { val vm: FilePickerViewModel = viewModel() val navController = rememberNavController() val navBarItems = remember { mutableStateListOf() } @@ -85,10 +91,17 @@ fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePic navController.popBackStack() } + fun navigateNewPath(path: String) { + vm.fileListStates[path] = FileListPageState() + navController.navigate(ROUTE_PAGE, Bundle().apply { + putString(ARG_PATH, path) + }) + } + fun update() { navController.popBackStack() navController.navigate(ROUTE_PAGE, Bundle().apply { - putString(ARG_URI, config.rootPath) + putString(ARG_PATH, config.rootPath) }) } @@ -98,9 +111,10 @@ fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePic LocalNavController provides navController, LocalFilePickerConfig provides config, ) { - var selectedCount by remember { mutableIntStateOf(0) } Column(modifier) { var sortConfig by remember { mutableStateOf(config.sortConfig) } + fun getState() = vm.fileListStates[vm.currentPath] + val selectedCount = getState()?.items?.count { it.isChecked.value } ?: 0 FilePickerToolbar( modifier = Modifier.fillMaxWidth(), title = navBarItems.lastOrNull()?.name ?: "", @@ -111,8 +125,13 @@ fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePic update() }, selectedCount = selectedCount, - onCancelSelect = { selectedCount = 0}, - onConfirmSelect = { } + onCancelSelect = { + getState()?.uncheckAll() + }, + onConfirmSelect = { + onConfirmSelect(getState()?.items?.filter { it.isChecked.value } + ?.map { it.model } ?: emptyList()) + } ) FileNavBar( @@ -121,7 +140,7 @@ fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePic onClick = { item -> while (true) { val uri = - navController.currentBackStackEntry?.arguments?.getString(ARG_URI) + navController.currentBackStackEntry?.arguments?.getString(ARG_PATH) ?: break if (uri == item.path) break else popBack() @@ -134,32 +153,36 @@ fun FilePicker(modifier: Modifier = Modifier, config: FilePickerConfig = FilePic startDestination = ROUTE_PAGE ) { composable(ROUTE_PAGE) { entry -> - LaunchedEffect(key1 = Unit) { - + navController.enableOnBackPressed(false) + val path = entry.arguments?.getString(ARG_PATH) ?: config.rootPath + val fileListState = vm.fileListStates[path] ?: FileListPageState().run { + vm.fileListStates[path] = this + this } - navController.enableOnBackPressed(false) - val uri = entry.arguments?.getString(ARG_URI) ?: config.rootPath - BackHandler(uri != config.rootPath) { + LaunchedEffect(key1 = Unit) { + vm.currentPath = path + } + BackHandler(path != config.rootPath) { popBack() } + BackHandler(selectedCount > 0) { + getState()?.uncheckAll() + } - val file = File(uri) + val file = File(path) if (navBarItems.isEmpty()) navBarItems.add(NavBarItem(name = file.name, path = file.path)) - FileListPage( file = NormalFile(file), + state = fileListState, onBack = { popBack() }, onEnter = { enterFile -> navBarItems += NavBarItem(name = enterFile.name, path = enterFile.path) - navController.navigate(ROUTE_PAGE, Bundle().apply { - putString(ARG_URI, enterFile.path) - }) + navigateNewPath(enterFile.path) }, - selectedCount = selectedCount, - onSelectedCountChange = { selectedCount = it } - ) + + ) } } diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerConfig.kt b/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerConfig.kt index 48d598f..995e676 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerConfig.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerConfig.kt @@ -10,7 +10,7 @@ fun interface FileFilter { } fun interface FileSelector { - fun select(file: IFileModel): Boolean + fun select(checkedList: List, check: IFileModel): Boolean } data class FilePickerConfig( @@ -18,7 +18,7 @@ data class FilePickerConfig( val fileDetector: FileDetector = FileDetector(), val fileFilter: FileFilter = FileFilter { true }, - val fileSelector: FileSelector = FileSelector { true }, + val fileSelector: FileSelector = FileSelector { _, _ -> true }, var sortConfig: SortConfig = SortConfig(), ) { diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerViewModel.kt b/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerViewModel.kt index 472f9d0..7f59c21 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerViewModel.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/FilePickerViewModel.kt @@ -1,7 +1,13 @@ package com.github.jing332.filepicker +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.github.jing332.filepicker.listpage.FileListPageState class FilePickerViewModel : ViewModel() { + var currentPath by mutableStateOf("") + val fileListStates = mutableMapOf() } \ No newline at end of file diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/FileListPage.kt b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPage.kt similarity index 78% rename from filepicker/src/main/java/com/github/jing332/filepicker/FileListPage.kt rename to filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPage.kt index 1203ad5..5953c09 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/FileListPage.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPage.kt @@ -1,4 +1,4 @@ -package com.github.jing332.filepicker +package com.github.jing332.filepicker.listpage import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi @@ -27,6 +27,8 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.github.jing332.filepicker.LocalFilePickerConfig +import com.github.jing332.filepicker.R import com.github.jing332.filepicker.model.IFileModel import com.github.jing332.filepicker.utils.performLongPress @@ -34,20 +36,21 @@ import com.github.jing332.filepicker.utils.performLongPress @Composable fun FileListPage( modifier: Modifier = Modifier, + state: FileListPageState = FileListPageState(), file: IFileModel, onBack: () -> Unit, onEnter: (IFileModel) -> Unit, - - selectedCount: Int, - onSelectedCountChange: (Int) -> Unit + vm: FileListPageViewModel = viewModel(key = file.name + "_" + file.path) ) { - val vm: FileListPageViewModel = viewModel(key = file.name + "_" + file.path) - val hasChecked by rememberUpdatedState(newValue = vm.hasChecked()) + val hasChecked by rememberUpdatedState(newValue = state.hasChecked()) val config = LocalFilePickerConfig.current val view = LocalView.current + LaunchedEffect(key1 = state) { + vm.state = state + } LaunchedEffect(key1 = file) { - if (vm.files.isEmpty()) + if (state.items.isEmpty()) vm.updateFiles(file, config) } @@ -55,24 +58,26 @@ fun FileListPage( if (hasChecked) view.performLongPress() } - LaunchedEffect(key1 = selectedCount) { - if (selectedCount == 0) - vm.cancelSelect() - } LazyColumn( modifier = modifier, - state = vm.listState + state = state.listState ) { - itemsIndexed(vm.files, key = { _, item -> item.key }) { _, item -> + itemsIndexed(state.items, key = { _, item -> item.key }) { _, item -> + fun isCheckable(): Boolean { + if (item.isBackType) return false + val checkedList = state.items.filter { it.isChecked.value }.map { it.model } + return config.fileSelector.select(checkedList, item.model) + } + fun check(checked: Boolean = !item.isChecked.value) { - item.isChecked.value = checked - onSelectedCountChange(vm.selectedCount()) + if (isCheckable()) + state.check(item, checked) } Item( isChecked = item.isChecked.value, - isCheckable = item.isCheckable.value, +// isCheckable = item.isCheckable.value, icon = { if (item.isDirectory) { Icon( @@ -111,26 +116,24 @@ fun FileListPage( ) } }, - onCheckedChange = { - item.isChecked.value = it + onCheckedChange = { checked -> + if (checked) { + if (isCheckable()) + check(true) + } else + item.isChecked.value = false }, onClick = { if (item.isBackType) onBack() else if (!hasChecked && !item.isChecked.value && item.isDirectory) onEnter(item.model) - else if (config.fileSelector.select(item.model)) - check() - else + else if (isCheckable()) check() }, onLongClick = { - if (item.isBackType) - onBack() - else if (!config.fileSelector.select(item.model)) return@Item - else if (item.isCheckable.value) { - check() - } + if (item.isBackType) onBack() + else if (isCheckable()) check() } ) } @@ -144,7 +147,7 @@ private fun Item( modifier: Modifier = Modifier, isChecked: Boolean = false, onCheckedChange: (Boolean) -> Unit, - isCheckable: Boolean = false, + isCheckable: Boolean = true, icon: @Composable () -> Unit, title: @Composable () -> Unit, subtitle: @Composable () -> Unit, diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageState.kt b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageState.kt new file mode 100644 index 0000000..f855094 --- /dev/null +++ b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageState.kt @@ -0,0 +1,100 @@ +package com.github.jing332.filepicker.listpage + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.lifecycle.viewModelScope +import com.github.jing332.filepicker.FileFilter +import com.github.jing332.filepicker.FilePickerConfig +import com.github.jing332.filepicker.SortConfig +import com.github.jing332.filepicker.SortType +import com.github.jing332.filepicker.model.BackFileModel +import com.github.jing332.filepicker.model.IFileModel +import com.github.jing332.filepicker.utils.StringUtils.sizeToReadable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Locale +import kotlin.system.measureTimeMillis + +@Composable +fun rememberFileListPageState() = remember { + FileListPageState() +} + +class FileListPageState { + val listState by lazy { LazyListState() } + + internal val items = mutableStateListOf() + + val selectedCount: MutableIntState = mutableIntStateOf(0) + + internal fun updateSelectedCount() { + selectedCount.intValue = items.count { it.isChecked.value } + } + + internal fun check(item: FileItem, checked: Boolean = true) { + item.isChecked.value = checked + } + + fun checkAll() { + for (item in items) { + check(item, true) + } + } + + fun uncheckAll() { + for (item in items) { + check(item, false) + } + } + + fun hasChecked(): Boolean { + return items.any { it.isChecked.value } + } + + private fun IFileModel.filesSortAndFilter( + sort: SortConfig, + filter: FileFilter + ): List { + return this.files().filter { filter.accept(it) }.sortedWith( + compareBy( + { !it.isDirectory }, + { + val str = when (sort.sortBy) { + SortType.NAME -> it.name + SortType.SIZE -> it.size.toString() + SortType.DATE -> it.time.toString() + SortType.TYPE -> it.name.split(".").lastOrNull() ?: "" + else -> it.name + } + str.lowercase(Locale.getDefault()) + } + ) + ).run { + if (sort.reverse) reversed() else this + } + } +} + +internal data class FileItem( + val model: IFileModel, + val key: String = model.path, + val name: String = model.name, + val isDirectory: Boolean = model.isDirectory, + val isBackType: Boolean = false, + + val isChecked: MutableState = mutableStateOf(false), + val isCheckable: MutableState = mutableStateOf(true), + + val fileCount: MutableIntState = mutableIntStateOf(0), + val fileSize: MutableState = mutableStateOf("0"), + val fileLastModified: MutableState = mutableStateOf(""), + + val icon: @Composable() (() -> Unit)? = null, +) \ No newline at end of file diff --git a/filepicker/src/main/java/com/github/jing332/filepicker/FileListPageViewModel.kt b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageViewModel.kt similarity index 59% rename from filepicker/src/main/java/com/github/jing332/filepicker/FileListPageViewModel.kt rename to filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageViewModel.kt index e76d958..a950c48 100644 --- a/filepicker/src/main/java/com/github/jing332/filepicker/FileListPageViewModel.kt +++ b/filepicker/src/main/java/com/github/jing332/filepicker/listpage/FileListPageViewModel.kt @@ -1,14 +1,12 @@ -package com.github.jing332.filepicker +package com.github.jing332.filepicker.listpage -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableIntState -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.github.jing332.filepicker.FileFilter +import com.github.jing332.filepicker.FilePickerConfig +import com.github.jing332.filepicker.SortConfig +import com.github.jing332.filepicker.SortType import com.github.jing332.filepicker.model.BackFileModel import com.github.jing332.filepicker.model.IFileModel import com.github.jing332.filepicker.utils.StringUtils.sizeToReadable @@ -24,14 +22,11 @@ class FileListPageViewModel : ViewModel() { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) } - val listState by lazy { LazyListState() } - internal val files = mutableStateListOf() + internal var state: FileListPageState? = null + internal val files: SnapshotStateList + get() = state?.items!! - fun hasChecked(): Boolean { - return files.any { it.isChecked.value } - } - private fun IFileModel.filesSortAndFilter( sort: SortConfig, filter: FileFilter @@ -64,9 +59,7 @@ class FileListPageViewModel : ViewModel() { val cost = measureTimeMillis { files += file.filesSortAndFilter(config.sortConfig, config.fileFilter).map { - FileItem(it).apply { - isCheckable.value = config.fileSelector.select(model) - } + FileItem(it) } } println("load files: $cost ms") @@ -84,35 +77,4 @@ class FileListPageViewModel : ViewModel() { } } } - - fun updateModel(item: FileItem) { - val index = files.indexOfFirst { it.key == item.key } - if (index != -1) - files[index] = item - } - - fun selectedCount(): Int { - return files.count { it.isChecked.value } - } - - fun cancelSelect() { - files.forEach { it.isChecked.value = false } - } } - -data class FileItem( - val model: IFileModel, - val key: String = model.path, - val name: String = model.name, - val isDirectory: Boolean = model.isDirectory, - val isBackType: Boolean = false, - - val isChecked: MutableState = mutableStateOf(false), - val isCheckable: MutableState = mutableStateOf(true), - - val fileCount: MutableIntState = mutableIntStateOf(0), - val fileSize: MutableState = mutableStateOf("0"), - val fileLastModified: MutableState = mutableStateOf(""), - - val icon: @Composable() (() -> Unit)? = null, -) \ No newline at end of file