diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d2ce159..9e3fc2be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Attention: don't forget to add the flag for F-Droid before release - [Feature] New File Manager listing and uploading - [Feature] Add vibration off switch - [Feature] New File Manager listing and uploading +- [Feature] New File Manager search - [Refactor] Load RemoteControls from flipper, emulating animation - [Refactor] Update to Kotlin 2.0 - [Refactor] Replace Ktorfit with Ktor requests in remote-controls diff --git a/components/bridge/connection/sample/build.gradle.kts b/components/bridge/connection/sample/build.gradle.kts index 938cf6433..c727fb7bc 100644 --- a/components/bridge/connection/sample/build.gradle.kts +++ b/components/bridge/connection/sample/build.gradle.kts @@ -82,6 +82,8 @@ dependencies { implementation(projects.components.filemngr.listing.impl) implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.upload.impl) + implementation(projects.components.filemngr.search.api) + implementation(projects.components.filemngr.search.impl) implementation(projects.components.newfilemanager.api) implementation(projects.components.newfilemanager.impl) diff --git a/components/core/ktx/src/commonMain/kotlin/com/flipperdevices/core/ktx/jre/DelayFlowKtx.kt b/components/core/ktx/src/commonMain/kotlin/com/flipperdevices/core/ktx/jre/DelayFlowKtx.kt new file mode 100644 index 000000000..f974f7bbe --- /dev/null +++ b/components/core/ktx/src/commonMain/kotlin/com/flipperdevices/core/ktx/jre/DelayFlowKtx.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.core.ktx.jre + +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex +import kotlin.time.Duration + +@OptIn(FlowPreview::class) +fun Flow.debounceAfterFirst(timeout: Duration): Flow { + return withIndex().debounce { + if (it.index == 0) { + 0L + } else { + timeout.inWholeMilliseconds + } + }.map { it.value } +} diff --git a/components/core/ui/searchbar/src/commonMain/kotlin/com/flipperdevices/core/ui/searchbar/ComposableSearchBar.kt b/components/core/ui/searchbar/src/commonMain/kotlin/com/flipperdevices/core/ui/searchbar/ComposableSearchBar.kt index 96a075e2b..52e883698 100644 --- a/components/core/ui/searchbar/src/commonMain/kotlin/com/flipperdevices/core/ui/searchbar/ComposableSearchBar.kt +++ b/components/core/ui/searchbar/src/commonMain/kotlin/com/flipperdevices/core/ui/searchbar/ComposableSearchBar.kt @@ -29,7 +29,7 @@ fun ComposableSearchBar( onBack: () -> Unit ) { var text by remember { mutableStateOf("") } - ComposableSearchBarInternal( + ComposableSearchBar( hint = hint, text = text, onChangeText = { @@ -41,14 +41,16 @@ fun ComposableSearchBar( } @Composable -private fun ComposableSearchBarInternal( +fun ComposableSearchBar( hint: String, text: String, onChangeText: (String) -> Unit, - onBack: () -> Unit + onBack: () -> Unit, + modifier: Modifier = Modifier ) { Row( - modifier = Modifier.background(LocalPallet.current.background) + modifier = modifier + .background(LocalPallet.current.background) .statusBarsPadding(), verticalAlignment = Alignment.CenterVertically ) { diff --git a/components/filemngr/listing/api/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt b/components/filemngr/listing/api/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt index 4beca2a9e..9cb8bc23b 100644 --- a/components/filemngr/listing/api/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt +++ b/components/filemngr/listing/api/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt @@ -9,12 +9,22 @@ abstract class FilesDecomposeComponent( componentContext: ComponentContext ) : ScreenDecomposeComponent(componentContext) { fun interface Factory { + @Suppress("LongParameterList") operator fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, path: Path, onPathChanged: (Path) -> Unit, - onUploadClick: () -> Unit + searchCallback: SearchCallback, + uploadCallback: UploadCallback ): FilesDecomposeComponent } + + fun interface SearchCallback { + fun invoke() + } + + fun interface UploadCallback { + fun invoke() + } } diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt index 920b6fd00..0dc60c98a 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt @@ -36,7 +36,8 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( @Assisted private val path: Path, @Assisted private val onBack: DecomposeOnBackParameter, @Assisted private val onPathChanged: (Path) -> Unit, - @Assisted private val onUploadClick: () -> Unit, + @Assisted private val searchCallback: SearchCallback, + @Assisted private val uploadCallback: UploadCallback, private val storageInfoViewModelFactory: Provider, private val optionsInfoViewModelFactory: Provider, private val editFileViewModelFactory: Provider, @@ -114,9 +115,10 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( storageInfoViewModel = storageInfoViewModel, selectionViewModel = selectionViewModel, onBack = onBack::invoke, - onUploadClick = onUploadClick, + onUploadClick = uploadCallback::invoke, onPathChange = onPathChanged, - onFileMoreClick = slotNavigation::activate + onFileMoreClick = slotNavigation::activate, + onSearchClick = searchCallback::invoke ) FileOptionsBottomSheet( fileOptionsSlot = fileOptionsSlot, diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt index 9b36c8c6e..0fb80c497 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt @@ -38,6 +38,7 @@ fun ComposableFileListScreen( selectionViewModel: SelectionViewModel, onBack: () -> Unit, onUploadClick: () -> Unit, + onSearchClick: () -> Unit, onPathChange: (Path) -> Unit, onFileMoreClick: (PathWithType) -> Unit, modifier: Modifier = Modifier @@ -61,7 +62,8 @@ fun ComposableFileListScreen( canCreateFiles = canCreateFiles, onUploadClick = onUploadClick, editFileViewModel = editFileViewModel, - onBack = onBack + onBack = onBack, + onSearchClick = onSearchClick ) } ) { contentPadding -> diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt index 202f39c90..407183742 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt @@ -1,6 +1,7 @@ package com.flipperdevices.filemanager.listing.impl.composable import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.items @@ -44,7 +45,8 @@ fun LazyGridScope.LoadedFilesComposable( FolderCardPlaceholderComposable( modifier = Modifier .fillMaxWidth() - .animateItemPlacement(), + .animateItem() + .animateContentSize(), orientation = orientation, ) } else { @@ -55,7 +57,8 @@ fun LazyGridScope.LoadedFilesComposable( FolderCardComposable( modifier = Modifier .fillMaxWidth() - .animateItemPlacement(), + .animateItem() + .animateContentSize(), painter = file.asPainter(), iconTint = file.asTint(), title = file.fileName, diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt index 05a9d5bb4..304618b66 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt @@ -7,11 +7,20 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType import com.flipperdevices.core.ui.ktx.OrangeAppBar +import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.filemanager.listing.impl.R import com.flipperdevices.filemanager.listing.impl.composable.MoreIconComposable @@ -21,7 +30,9 @@ import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.OptionsViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.SelectionViewModel import okio.Path +import com.flipperdevices.core.ui.res.R as DesignSystem +@Suppress("LongMethod") @Composable fun FileListAppBar( selectionState: SelectionViewModel.State, @@ -32,13 +43,13 @@ fun FileListAppBar( optionsViewModel: OptionsViewModel, canCreateFiles: Boolean, onUploadClick: () -> Unit, + onSearchClick: () -> Unit, editFileViewModel: EditFileViewModel, onBack: () -> Unit, modifier: Modifier = Modifier ) { AnimatedContent( - modifier = modifier - .background(LocalPalletV2.current.surface.navBar.body.accentBrand), + modifier = modifier.background(LocalPalletV2.current.surface.navBar.body.accentBrand), targetState = selectionState.isEnabled, contentKey = { it }, transitionSpec = { @@ -49,39 +60,51 @@ fun FileListAppBar( ) { isSelectionEnabled -> if (isSelectionEnabled) { CloseSelectionAppBar( + onDeselectAll = selectionViewModel::deselectAll, onClose = selectionViewModel::toggleMode, onSelectAll = { val paths = (filesListState as? FilesViewModel.State.Loaded) ?.files .orEmpty() .map { - val fullPath = path.resolve(it.fileName) PathWithType( fileType = it.fileType ?: FileType.FILE, - fullPath = fullPath + fullPath = path.resolve(it.fileName) ) } selectionViewModel.select(paths) }, - onDeselectAll = selectionViewModel::deselectAll ) } else { OrangeAppBar( title = stringResource(R.string.fml_appbar_title), endBlock = { - MoreIconComposable( - optionsState = optionsState, - onAction = optionsViewModel::onAction, - canCreateFiles = canCreateFiles, - onUploadClick = onUploadClick, - onSelectClick = selectionViewModel::toggleMode, - onCreateFolderClick = { - editFileViewModel.onCreate(path, FileType.DIR) - }, - onCreateFileClick = { - editFileViewModel.onCreate(path, FileType.FILE) - } - ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .size(24.dp) + .clickableRipple(onClick = onSearchClick), + painter = painterResource(DesignSystem.drawable.ic_search), + contentDescription = null, + tint = Color.Unspecified + ) + MoreIconComposable( + optionsState = optionsState, + onAction = optionsViewModel::onAction, + canCreateFiles = canCreateFiles, + onUploadClick = onUploadClick, + onSelectClick = selectionViewModel::toggleMode, + onCreateFolderClick = { + editFileViewModel.onCreate(path, FileType.DIR) + }, + onCreateFileClick = { + editFileViewModel.onCreate(path, FileType.FILE) + } + ) + } }, onBack = onBack::invoke, ) diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt index 8459ebf4c..68ef96e65 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt @@ -38,7 +38,7 @@ import com.flipperdevices.filemanager.listing.impl.R as FML import com.flipperdevices.filemanager.ui.components.R as FR @Composable -private fun MoreBototmBarOptions( +private fun MoreBottomBarOptions( onCopyTo: () -> Unit, canRename: Boolean, onRename: () -> Unit, @@ -115,7 +115,7 @@ fun BottomBarOptions( painter = painterResource(FR.drawable.ic_upload), onClick = onExport ) - MoreBototmBarOptions( + MoreBottomBarOptions( onCopyTo = onCopyTo, canRename = canRename, onRename = onRename, diff --git a/components/filemngr/main/impl/build.gradle.kts b/components/filemngr/main/impl/build.gradle.kts index 7ed954d93..29f2dcd0a 100644 --- a/components/filemngr/main/impl/build.gradle.kts +++ b/components/filemngr/main/impl/build.gradle.kts @@ -24,9 +24,10 @@ androidDependencies { implementation(projects.components.filemngr.uiComponents) implementation(projects.components.filemngr.main.api) - implementation(projects.components.newfilemanager.api) implementation(projects.components.filemngr.listing.api) implementation(projects.components.filemngr.upload.api) + implementation(projects.components.filemngr.search.api) + implementation(projects.components.newfilemanager.api) // Compose implementation(libs.compose.ui) diff --git a/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt b/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt index 67a1265cc..bf40f9587 100644 --- a/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt +++ b/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt @@ -4,11 +4,13 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.replaceAll import com.arkivanov.decompose.router.stack.replaceCurrent import com.flipperdevices.core.di.AppGraph import com.flipperdevices.filemanager.listing.api.FilesDecomposeComponent import com.flipperdevices.filemanager.main.api.FileManagerDecomposeComponent import com.flipperdevices.filemanager.main.impl.model.FileManagerNavigationConfig +import com.flipperdevices.filemanager.search.api.SearchDecomposeComponent import com.flipperdevices.filemanager.upload.api.UploadDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter @@ -24,6 +26,7 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( @Assisted private val onBack: DecomposeOnBackParameter, private val filesDecomposeComponentFactory: FilesDecomposeComponent.Factory, private val uploadDecomposeComponentFactory: UploadDecomposeComponent.Factory, + private val searchDecomposeComponentFactory: SearchDecomposeComponent.Factory, ) : FileManagerDecomposeComponent(), ComponentContext by componentContext { @@ -45,7 +48,8 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( path = config.path, onBack = { navigation.popOr(onBack::invoke) }, onPathChanged = { navigation.replaceCurrent(FileManagerNavigationConfig.FileTree(it)) }, - onUploadClick = { navigation.pushNew(FileManagerNavigationConfig.Upload(config.path)) } + uploadCallback = { navigation.pushNew(FileManagerNavigationConfig.Upload(config.path)) }, + searchCallback = { navigation.pushNew(FileManagerNavigationConfig.Search(config.path)) }, ) } @@ -56,5 +60,14 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( onFinish = navigation::pop ) } + + is FileManagerNavigationConfig.Search -> { + searchDecomposeComponentFactory.invoke( + componentContext = componentContext, + path = config.path, + onBack = navigation::pop, + onFolderSelect = { navigation.replaceAll(FileManagerNavigationConfig.FileTree(it)) } + ) + } } } diff --git a/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt b/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt index 3138ef55b..74e83659a 100644 --- a/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt +++ b/components/filemngr/main/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt @@ -21,6 +21,12 @@ sealed interface FileManagerNavigationConfig { val path: Path ) : FileManagerNavigationConfig + @Serializable + data class Search( + @Serializable(with = PathSerializer::class) + val path: Path + ) : FileManagerNavigationConfig + companion object { val DefaultFileTree: FileTree get() = FileTree("/".toPath()) diff --git a/components/filemngr/search/api/build.gradle.kts b/components/filemngr/search/api/build.gradle.kts new file mode 100644 index 000000000..23fa48f75 --- /dev/null +++ b/components/filemngr/search/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") +} + +android.namespace = "com.flipperdevices.filemanager.search.api" + +commonDependencies { + implementation(projects.components.core.ui.decompose) + + implementation(libs.compose.ui) + implementation(libs.decompose) + + implementation(libs.okio) +} diff --git a/components/filemngr/search/api/src/commonMain/kotlin/com/flipperdevices/filemanager/search/api/SearchDecomposeComponent.kt b/components/filemngr/search/api/src/commonMain/kotlin/com/flipperdevices/filemanager/search/api/SearchDecomposeComponent.kt new file mode 100644 index 000000000..9013d2d18 --- /dev/null +++ b/components/filemngr/search/api/src/commonMain/kotlin/com/flipperdevices/filemanager/search/api/SearchDecomposeComponent.kt @@ -0,0 +1,23 @@ +package com.flipperdevices.filemanager.search.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import okio.Path + +abstract class SearchDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + path: Path, + onBack: DecomposeOnBackParameter, + onFolderSelect: FolderSelectCallback + ): SearchDecomposeComponent + } + + fun interface FolderSelectCallback { + fun invoke(path: Path) + } +} diff --git a/components/filemngr/search/impl/build.gradle.kts b/components/filemngr/search/impl/build.gradle.kts new file mode 100644 index 000000000..14fb6039f --- /dev/null +++ b/components/filemngr/search/impl/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.filemanager.search.impl" + +androidDependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + implementation(projects.components.core.preference) + + implementation(projects.components.core.ui.lifecycle) + implementation(projects.components.core.ui.theme) + implementation(projects.components.core.ui.decompose) + implementation(projects.components.core.ui.ktx) + implementation(projects.components.core.ui.res) + implementation(projects.components.core.ui.dialog) + implementation(projects.components.core.ui.searchbar) + + implementation(projects.components.bridge.dao.api) + implementation(projects.components.bridge.service.api) + implementation(projects.components.bridge.pbutils) + implementation(projects.components.bridge.api) + + implementation(projects.components.bridge.connection.feature.common.api) + implementation(projects.components.bridge.connection.transport.common.api) + implementation(projects.components.bridge.connection.feature.provider.api) + implementation(projects.components.bridge.connection.feature.storage.api) + implementation(projects.components.bridge.connection.feature.storageinfo.api) + implementation(projects.components.bridge.connection.feature.serialspeed.api) + implementation(projects.components.bridge.connection.feature.rpcinfo.api) + implementation(projects.components.bridge.dao.api) + + implementation(projects.components.filemngr.uiComponents) + implementation(projects.components.filemngr.search.api) + implementation(projects.components.filemngr.main.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + implementation(libs.compose.material3) + implementation(libs.compose.material.icons.core) + implementation(libs.compose.material.icons.extended) + + implementation(libs.kotlin.serialization.json) + implementation(libs.ktor.client) + + implementation(libs.decompose) + implementation(libs.kotlin.coroutines) + implementation(libs.essenty.lifecycle) + implementation(libs.essenty.lifecycle.coroutines) + + implementation(libs.bundles.decompose) + implementation(libs.okio) + implementation(libs.kotlin.immutable.collections) +} + +androidUnitTestDependencies { + implementation(projects.components.core.test) + implementation(libs.junit) + implementation(libs.ktx.testing) +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/api/SearchDecomposeComponentImpl.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/api/SearchDecomposeComponentImpl.kt new file mode 100644 index 000000000..a966eedae --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/api/SearchDecomposeComponentImpl.kt @@ -0,0 +1,67 @@ +package com.flipperdevices.filemanager.search.impl.api + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory +import com.flipperdevices.core.ui.searchbar.ComposableSearchBar +import com.flipperdevices.filemanager.search.api.SearchDecomposeComponent +import com.flipperdevices.filemanager.search.impl.composable.ComposableFilesSearchScreen +import com.flipperdevices.filemanager.search.impl.viewmodel.SearchViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.gulya.anvil.assisted.ContributesAssistedFactory +import okio.Path + +@ContributesAssistedFactory(AppGraph::class, SearchDecomposeComponent.Factory::class) +class SearchDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val path: Path, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted private val onFolderSelect: FolderSelectCallback, + private val searchViewModelFactory: SearchViewModel.Factory +) : SearchDecomposeComponent(componentContext) { + + @Composable + override fun Render() { + val searchViewModel = viewModelWithFactory(path) { + searchViewModelFactory.invoke(path) + } + val rootSearchViewModel = path.root + .takeIf { !path.isRoot } + ?.let { rootPath -> + viewModelWithFactory(rootPath) { + searchViewModelFactory.invoke(rootPath) + } + } + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + val queryState by searchViewModel.queryState.collectAsState() + ComposableSearchBar( + hint = "", + text = queryState.query, + onChangeText = { + searchViewModel.onQueryChange(it) + rootSearchViewModel?.onQueryChange(it) + }, + onBack = onBack::invoke + ) + } + ) { contentPadding -> + ComposableFilesSearchScreen( + searchViewModel = searchViewModel, + rootSearchViewModel = rootSearchViewModel, + onFolderSelect = onFolderSelect::invoke, + modifier = Modifier.padding(contentPadding) + ) + } + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt new file mode 100644 index 000000000..05dfe967f --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt @@ -0,0 +1,66 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.filemanager.search.impl.viewmodel.SearchViewModel +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterIsInstance +import okio.Path + +@Composable +fun ComposableFilesSearchScreen( + searchViewModel: SearchViewModel, + rootSearchViewModel: SearchViewModel?, + onFolderSelect: (Path) -> Unit, + modifier: Modifier = Modifier +) { + val searchState by searchViewModel.state.collectAsState() + val rootSearchState by (rootSearchViewModel?.state ?: emptyFlow()) + .filterIsInstance() + .collectAsState(null) + + Column(modifier = modifier) { + AnimatedContent( + modifier = Modifier.fillMaxSize(), + targetState = searchState, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + contentKey = { it::class.simpleName }, + ) { currentDirState -> + when (currentDirState) { + is SearchViewModel.State.Loaded -> { + SearchItemsComposable( + currentDirState = currentDirState, + rootSearchState = rootSearchState, + onFolderSelect = onFolderSelect, + ) + } + + SearchViewModel.State.Loading -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(14.dp), + ) { + FilesPlaceholderComposable() + } + } + + SearchViewModel.State.Unsupported -> { + NoListingFeatureComposable() + } + } + } + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FilesPlaceholderComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FilesPlaceholderComposable.kt new file mode 100644 index 000000000..351ea6577 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FilesPlaceholderComposable.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.flipperdevices.core.preference.pb.FileManagerOrientation +import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardPlaceholderComposable + +@Suppress("FunctionNaming") +fun LazyListScope.FilesPlaceholderComposable() { + items(count = 6) { + FolderCardPlaceholderComposable( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + orientation = FileManagerOrientation.LIST, + ) + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardListLazyComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardListLazyComposable.kt new file mode 100644 index 000000000..c84b770d2 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardListLazyComposable.kt @@ -0,0 +1,50 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.Modifier +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.core.ktx.jre.toFormattedSize +import com.flipperdevices.filemanager.search.impl.viewmodel.SearchViewModel +import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardListComposable +import com.flipperdevices.filemanager.ui.components.itemcard.components.asPainter +import com.flipperdevices.filemanager.ui.components.itemcard.components.asTint +import com.flipperdevices.filemanager.ui.components.itemcard.model.ItemUiSelectionState +import okio.Path + +@Suppress("FunctionNaming") +fun LazyListScope.FolderCardListLazyComposable( + searchState: SearchViewModel.State.Loaded, + onFolderSelect: (Path) -> Unit, +) { + items(searchState.items, key = { it.fullPath.toString() }) { file -> + FolderCardListComposable( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + painter = file.instance.asPainter(), + iconTint = file.instance.asTint(), + title = file.instance.fileName, + subtitle = file.fullPath.parent + ?.toString() + ?: file.instance.size.toFormattedSize(), + selectionState = ItemUiSelectionState.NONE, + onClick = { + when (file.instance.fileType) { + FileType.DIR -> onFolderSelect.invoke(file.fullPath) + + FileType.FILE -> + file.fullPath + .parent + ?.run(onFolderSelect::invoke) + + else -> Unit + } + }, + showEndBox = false, + onCheckChange = {}, + onMoreClick = {}, + ) + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardPlaceholderLazyComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardPlaceholderLazyComposable.kt new file mode 100644 index 000000000..e5651815f --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/FolderCardPlaceholderLazyComposable.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.flipperdevices.core.preference.pb.FileManagerOrientation +import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardPlaceholderComposable + +@Suppress("FunctionNaming") +fun LazyListScope.FolderCardPlaceholderLazyComposable() { + items(count = 6) { + FolderCardPlaceholderComposable( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + orientation = FileManagerOrientation.LIST, + ) + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ListingTitleComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ListingTitleComposable.kt new file mode 100644 index 000000000..5b7acf8a2 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ListingTitleComposable.kt @@ -0,0 +1,52 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.filemanager.ui.components.R +import okio.Path + +@Composable +fun ListingTitleComposable( + path: Path, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .padding(horizontal = 14.dp) + .padding(top = 4.dp) + .padding(bottom = 10.dp), + contentAlignment = Alignment.Center + ) { + if (path.parent != null) { + Text( + text = path.name, + style = LocalTypography.current.titleSB16, + color = LocalPalletV2.current.text.title.primary + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource( + when { + MaterialTheme.colors.isLight -> R.drawable.ic_sd_card_ok_black + else -> R.drawable.ic_sd_card_ok_white + } + ), + tint = Color.Unspecified, + contentDescription = null, + ) + } + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesComposable.kt new file mode 100644 index 000000000..ee7c7fede --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesComposable.kt @@ -0,0 +1,63 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.MaterialTheme +import androidx.compose.material.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.filemanager.search.impl.R as FMS +import com.flipperdevices.filemanager.ui.components.R as FR + +@Composable +fun NoFilesComposable( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(FMS.string.fms_no_files), + style = LocalTypography.current.titleB18, + color = LocalPalletV2.current.text.title.primary + ) + Image( + painter = painterResource( + when { + MaterialTheme.colors.isLight -> FR.drawable.ic__no_files_white + else -> FR.drawable.ic__no_files_black + } + ), + contentDescription = null, + modifier = Modifier.height(100.dp) + ) + } + } +} + +@Preview +@Composable +private fun NoFilesComposablePreview() { + FlipperThemeInternal { + NoFilesComposable() + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesLazyComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesLazyComposable.kt new file mode 100644 index 000000000..4fef15846 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoFilesLazyComposable.kt @@ -0,0 +1,18 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.flipperdevices.filemanager.search.impl.viewmodel.SearchViewModel + +@Suppress("FunctionNaming") +fun LazyListScope.NoFilesLazyComposable(searchState: SearchViewModel.State.Loaded) { + if (!searchState.isSearching && searchState.items.isEmpty()) { + item { + NoFilesComposable( + modifier = Modifier + .fillParentMaxSize() + .animateItem() + ) + } + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt new file mode 100644 index 000000000..7b365ddd2 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt @@ -0,0 +1,15 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun NoListingFeatureComposable(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize()) { + // todo + Text("Unsopported") + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/SearchItemsComposable.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/SearchItemsComposable.kt new file mode 100644 index 000000000..a85f7ec00 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/SearchItemsComposable.kt @@ -0,0 +1,51 @@ +package com.flipperdevices.filemanager.search.impl.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.filemanager.search.impl.viewmodel.SearchViewModel +import okio.Path + +@Composable +fun SearchItemsComposable( + currentDirState: SearchViewModel.State.Loaded, + rootSearchState: SearchViewModel.State.Loaded?, + onFolderSelect: (Path) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(14.dp), + ) { + item { ListingTitleComposable(currentDirState.path) } + + FolderCardListLazyComposable( + searchState = currentDirState, + onFolderSelect = onFolderSelect + ) + NoFilesLazyComposable(currentDirState) + + if (currentDirState.isSearching) { + FolderCardPlaceholderLazyComposable() + } + + rootSearchState?.let { rootSearchState -> + item { ListingTitleComposable(rootSearchState.path) } + + FolderCardListLazyComposable( + searchState = rootSearchState, + onFolderSelect = onFolderSelect + ) + NoFilesLazyComposable(rootSearchState) + + if (rootSearchState.isSearching) { + FolderCardPlaceholderLazyComposable() + } + } + } +} diff --git a/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/viewmodel/SearchViewModel.kt b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/viewmodel/SearchViewModel.kt new file mode 100644 index 000000000..0671cb317 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/search/impl/viewmodel/SearchViewModel.kt @@ -0,0 +1,170 @@ +package com.flipperdevices.filemanager.search.impl.viewmodel + +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureProvider +import com.flipperdevices.bridge.connection.feature.provider.api.FFeatureStatus +import com.flipperdevices.bridge.connection.feature.provider.api.get +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.bridge.connection.feature.storage.api.fm.FListingStorageApi +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.core.ktx.jre.debounceAfterFirst +import com.flipperdevices.core.ktx.jre.launchWithLock +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import okio.Path +import kotlin.time.Duration.Companion.milliseconds + +class SearchViewModel @AssistedInject constructor( + featureProvider: FFeatureProvider, + @Assisted private val path: Path +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "SearchViewModel" + private val mutex = Mutex() + + private val _state = MutableStateFlow(State.Loading) + val state = _state.asStateFlow() + + private val _searchState = MutableStateFlow(SearchState()) + val queryState = _searchState.asStateFlow() + + private var lastSearchJob: Job? = null + + private val featureState = featureProvider.get() + .stateIn(viewModelScope, SharingStarted.Eagerly, FFeatureStatus.Retrieving) + + fun onQueryChange(query: String) { + _searchState.update { it.copy(query = query) } + } + + private suspend fun itemsFlow( + listingApi: FListingStorageApi, + path: Path + ): Flow> = listingApi + .lsFlow(path.toString()) + .mapNotNull { items -> items.getOrNull() } + .map { items -> + items.map { item -> + SearchItem( + instance = item, + fullPath = path.resolve(item.fileName) + ) + } + } + + private fun itemsFlowRecursive( + listingApi: FListingStorageApi, + path: Path + ): Flow> = flow { + itemsFlow(listingApi, path) + .onEach { searchItems -> emit(searchItems) } + .onEach { searchItems -> + searchItems + .filter { searchItem -> searchItem.instance.fileType == FileType.DIR } + .forEach { searchItem -> + itemsFlowRecursive(listingApi, searchItem.fullPath) + .onEach { searchItems -> emit(searchItems) } + .collect() + } + }.collect() + } + + private fun trySearch(listingApi: FListingStorageApi) { + viewModelScope.launch { + lastSearchJob?.cancelAndJoin() + launchWithLock(mutex, viewModelScope, "search") { + lastSearchJob = coroutineContext.job + val query = queryState.value.query + _state.emit(State.Loaded(path = path, isSearching = true)) + val itemsFlow = when { + query.isEmpty() -> itemsFlow(listingApi, path) + else -> itemsFlowRecursive(listingApi, path) + } + itemsFlow.onEach { + val filteredItems = it.filter { item -> + if (query.isEmpty()) { + true + } else { + item.fullPath.name.contains(query, ignoreCase = true) + } + } + _state.update { oldState -> + val state = (oldState as? State.Loaded) ?: State.Loaded( + isSearching = true, + path = path + ) + state.copy(items = state.items.plus(filteredItems).toImmutableList()) + } + }.collect() + _state.update { oldState -> + val state = (oldState as? State.Loaded) ?: State.Loaded(path = path) + state.copy(isSearching = false) + } + } + } + } + + init { + combine( + flow = featureState, + flow2 = _searchState.debounceAfterFirst(timeout = 1000.milliseconds), + transform = { featureState, _ -> + when (featureState) { + FFeatureStatus.NotFound -> _state.emit(State.Unsupported) + FFeatureStatus.Retrieving -> _state.emit(State.Loading) + FFeatureStatus.Unsupported -> _state.emit(State.Unsupported) + is FFeatureStatus.Supported -> trySearch(featureState.featureApi.listingApi()) + } + } + ).launchIn(viewModelScope) + } + + data class SearchState( + val query: String = "" + ) + + sealed interface State { + data object Loading : State + data object Unsupported : State + data class Loaded( + val items: ImmutableList = persistentListOf(), + val isSearching: Boolean = false, + val path: Path + ) : State + } + + data class SearchItem( + val instance: ListingItem, + val fullPath: Path + ) + + @AssistedFactory + fun interface Factory { + operator fun invoke( + path: Path + ): SearchViewModel + } +} diff --git a/components/filemngr/search/impl/src/androidMain/res/values/strings.xml b/components/filemngr/search/impl/src/androidMain/res/values/strings.xml new file mode 100644 index 000000000..2960491c1 --- /dev/null +++ b/components/filemngr/search/impl/src/androidMain/res/values/strings.xml @@ -0,0 +1,4 @@ + + + No Files Yet + \ No newline at end of file diff --git a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt index 970fac7cf..3cfd83fb7 100644 --- a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt +++ b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt @@ -115,6 +115,7 @@ fun FolderCardListComposable( onMoreClick: () -> Unit, modifier: Modifier = Modifier, iconTint: Color = Color.Unspecified, + showEndBox: Boolean = true ) { Row( modifier = modifier @@ -146,11 +147,13 @@ fun FolderCardListComposable( } } - ItemCardEndBox( - selectionState = selectionState, - onCheckChange = onCheckChange, - onMoreClick = onMoreClick - ) + if (showEndBox) { + ItemCardEndBox( + selectionState = selectionState, + onCheckChange = onCheckChange, + onMoreClick = onMoreClick + ) + } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5f8ed0869..d8263d5b2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -96,6 +96,8 @@ include( ":components:filemngr:listing:impl", ":components:filemngr:upload:api", ":components:filemngr:upload:impl", + ":components:filemngr:search:api", + ":components:filemngr:search:impl", ":components:core:di", ":components:core:ktx",