diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f74de5043..933e26366c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [Feature] Add count subfolders for new file manager - [Feature] Add file downloading for new file manager +- [Refactor] Move rename and file create to separated modules - [FIX] Migrate url host from metric.flipperdevices.com to metric.flipp.dev - [FIX] Fix empty response in faphub category - [FIX] New file manager uploading progress diff --git a/components/bridge/connection/sample/build.gradle.kts b/components/bridge/connection/sample/build.gradle.kts index 31b09ad358..7febdd4d8b 100644 --- a/components/bridge/connection/sample/build.gradle.kts +++ b/components/bridge/connection/sample/build.gradle.kts @@ -89,6 +89,10 @@ dependencies { implementation(projects.components.filemngr.editor.impl) implementation(projects.components.filemngr.download.api) implementation(projects.components.filemngr.download.impl) + implementation(projects.components.filemngr.rename.api) + implementation(projects.components.filemngr.rename.impl) + implementation(projects.components.filemngr.create.api) + implementation(projects.components.filemngr.create.impl) implementation(projects.components.newfilemanager.api) implementation(projects.components.newfilemanager.impl) diff --git a/components/filemngr/create/api/build.gradle.kts b/components/filemngr/create/api/build.gradle.kts new file mode 100644 index 0000000000..4f3654c92c --- /dev/null +++ b/components/filemngr/create/api/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") +} + +android.namespace = "com.flipperdevices.filemanager.create.api" + +commonDependencies { + implementation(projects.components.bridge.connection.feature.storage.api) + + implementation(projects.components.core.ui.decompose) + + implementation(libs.compose.ui) + implementation(libs.decompose) + + implementation(libs.okio) +} diff --git a/components/filemngr/create/api/src/commonMain/kotlin/com/flipperdevices/filemanager/create/api/CreateFileDecomposeComponent.kt b/components/filemngr/create/api/src/commonMain/kotlin/com/flipperdevices/filemanager/create/api/CreateFileDecomposeComponent.kt new file mode 100644 index 0000000000..5781990cc7 --- /dev/null +++ b/components/filemngr/create/api/src/commonMain/kotlin/com/flipperdevices/filemanager/create/api/CreateFileDecomposeComponent.kt @@ -0,0 +1,31 @@ +package com.flipperdevices.filemanager.create.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import kotlinx.coroutines.flow.StateFlow +import okio.Path + +abstract class CreateFileDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + abstract val canCreateFiles: StateFlow + + abstract fun startCreateFile(parent: Path) + + abstract fun startCreateFolder(parent: Path) + + abstract fun startCreate(parent: Path, type: FileType) + + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + createCallback: CreatedCallback, + ): CreateFileDecomposeComponent + } + + fun interface CreatedCallback { + fun invoke(item: ListingItem) + } +} diff --git a/components/filemngr/create/impl/build.gradle.kts b/components/filemngr/create/impl/build.gradle.kts new file mode 100644 index 0000000000..480e390edb --- /dev/null +++ b/components/filemngr/create/impl/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.filemanager.create.impl" + +commonDependencies { + 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.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.util) + implementation(projects.components.filemngr.uiComponents) + implementation(projects.components.filemngr.create.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + + 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) +} diff --git a/components/filemngr/create/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/create/impl/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..c50f7680bb --- /dev/null +++ b/components/filemngr/create/impl/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,7 @@ + + + Enter Name: + Create File + Create Folder + Allowed characters: %1$s + \ No newline at end of file diff --git a/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/api/CreateFileDecomposeComponentImpl.kt b/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/api/CreateFileDecomposeComponentImpl.kt new file mode 100644 index 0000000000..9b2125e47a --- /dev/null +++ b/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/api/CreateFileDecomposeComponentImpl.kt @@ -0,0 +1,92 @@ +package com.flipperdevices.filemanager.create.impl.api + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.filemanager.create.api.CreateFileDecomposeComponent +import com.flipperdevices.filemanager.create.impl.viewmodel.CreateFileViewModel +import com.flipperdevices.filemanager.ui.components.name.NameDialog +import com.flipperdevices.filemanager.util.constant.FileManagerConstants +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import flipperapp.components.filemngr.create.impl.generated.resources.fmc_create_file_allowed_chars +import flipperapp.components.filemngr.create.impl.generated.resources.fmc_create_file_title +import flipperapp.components.filemngr.create.impl.generated.resources.fml_create_file_btn +import flipperapp.components.filemngr.create.impl.generated.resources.fml_create_folder_btn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.gulya.anvil.assisted.ContributesAssistedFactory +import okio.Path +import org.jetbrains.compose.resources.stringResource +import javax.inject.Provider +import flipperapp.components.filemngr.create.impl.generated.resources.Res as FMC + +@ContributesAssistedFactory(AppGraph::class, CreateFileDecomposeComponent.Factory::class) +class CreateFileDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted val createdCallback: CreatedCallback, + renameViewModelProvider: Provider +) : CreateFileDecomposeComponent(componentContext) { + private val createFileViewModel = instanceKeeper.getOrCreate { + renameViewModelProvider.get() + } + + override val canCreateFiles = createFileViewModel.canCreateFiles + + override fun startCreateFile(parent: Path) { + createFileViewModel.startCreateFile(parent) + } + + override fun startCreateFolder(parent: Path) { + createFileViewModel.startCreateFolder(parent) + } + + override fun startCreate(parent: Path, type: FileType) { + createFileViewModel.startCreate(parent, type) + } + + @Composable + override fun Render() { + val state by createFileViewModel.state.collectAsState() + LaunchedEffect(createFileViewModel) { + createFileViewModel.event + .onEach { event -> + when (event) { + is CreateFileViewModel.Event.Created -> { + createdCallback.invoke(event.item) + } + } + }.launchIn(this) + } + when (val localState = state) { + CreateFileViewModel.State.Pending -> Unit + is CreateFileViewModel.State.Creating -> { + NameDialog( + value = localState.name, + title = stringResource(FMC.string.fmc_create_file_title), + buttonText = when (localState.type) { + FileType.FILE -> stringResource(FMC.string.fml_create_file_btn) + FileType.DIR -> stringResource(FMC.string.fml_create_folder_btn) + }, + subtitle = stringResource( + FMC.string.fmc_create_file_allowed_chars, + FileManagerConstants.FILE_NAME_AVAILABLE_CHARACTERS + ), + onFinish = createFileViewModel::onConfirm, + isError = !localState.isValid, + isEnabled = !localState.isCreating, + needShowOptions = localState.needShowOptions, + onTextChange = createFileViewModel::onNameChange, + onDismissRequest = createFileViewModel::dismiss, + onOptionSelect = createFileViewModel::onOptionSelected, + options = localState.options + ) + } + } + } +} diff --git a/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/viewmodel/CreateFileViewModel.kt b/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/viewmodel/CreateFileViewModel.kt new file mode 100644 index 0000000000..994a225863 --- /dev/null +++ b/components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/viewmodel/CreateFileViewModel.kt @@ -0,0 +1,200 @@ +package com.flipperdevices.filemanager.create.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.FFileUploadApi +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.bridge.connection.feature.storage.api.model.StorageRequestPriority +import com.flipperdevices.core.ktx.jre.FlipperFileNameValidator +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.filemanager.util.constant.FileManagerConstants +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okio.ByteString +import okio.Path +import okio.buffer +import javax.inject.Inject + +class CreateFileViewModel @Inject constructor( + private val featureProvider: FFeatureProvider, +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "CreateFileViewModel" + + private val fileNameValidator = FlipperFileNameValidator() + + private val eventChannel = Channel() + val event = eventChannel.receiveAsFlow() + + private val _state = MutableStateFlow(State.Pending) + val state = _state.asStateFlow() + + private var featureJob: Job? = null + private val featureMutex = Mutex() + + val canCreateFiles = featureProvider.get() + .filterIsInstance>() + .map { true } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + fun startCreate(parent: Path, type: FileType) { + _state.update { + State.Creating( + parent = parent, + type = type, + name = "", + isValid = false, + isCreating = false + ) + } + } + + fun startCreateFile(parent: Path) { + startCreate(parent, FileType.FILE) + } + + fun startCreateFolder(parent: Path) { + startCreate(parent, FileType.DIR) + } + + fun dismiss() { + _state.update { State.Pending } + } + + fun onNameChange(name: String) { + _state.update { state -> + (state as? State.Creating) + ?.copy(name = name) + ?: state + } + } + + fun onOptionSelected(index: Int) { + _state.update { state -> + (state as? State.Creating)?.let { renamingState -> + val option = renamingState.options.getOrNull(index).orEmpty() + renamingState.copy(name = "${renamingState.name}.$option") + } ?: state + } + } + + private suspend fun FFileUploadApi.createNewFile(pathOnFlipper: String): Result { + return runCatching { + sink( + pathOnFlipper = pathOnFlipper, + priority = StorageRequestPriority.FOREGROUND + ).buffer().use { bufferedSink -> bufferedSink.write(ByteString.of()) } + } + } + + private suspend fun create(uploadApi: FFileUploadApi) { + val state = (state.first() as? State.Creating) + if (state == null) { + error { "#rename state was not Renaming" } + return + } + val fullPath = state.parent.resolve(state.name) + _state.emit(state.copy(isCreating = true)) + when (state.type) { + FileType.FILE -> { + uploadApi.createNewFile(fullPath.toString()) + } + + FileType.DIR -> { + uploadApi.mkdir(fullPath.toString()) + } + }.onSuccess { + val item = ListingItem( + fileType = state.type, + fileName = fullPath.name, + size = 0L + ) + val event = Event.Created(item) + eventChannel.send(event) + }.onFailure { + error(it) { "Could not create file $fullPath" } + } + _state.emit(State.Pending) + featureJob?.cancelAndJoin() + } + + fun onConfirm() { + viewModelScope.launch { + featureJob?.cancelAndJoin() + featureMutex.withLock { + featureJob = featureProvider.get() + .onEach { status -> + when (status) { + FFeatureStatus.NotFound -> Unit + FFeatureStatus.Retrieving -> Unit + FFeatureStatus.Unsupported -> Unit + is FFeatureStatus.Supported -> { + create(status.featureApi.uploadApi()) + } + } + }.launchIn(viewModelScope) + featureJob?.join() + } + } + } + + private fun collectNameValidation() { + state + .filterIsInstance() + .distinctUntilChangedBy { state -> state.name } + .onEach { state -> + _state.emit(state.copy(isValid = fileNameValidator.isValid(state.name))) + }.launchIn(viewModelScope) + } + + init { + collectNameValidation() + } + + sealed interface State { + data object Pending : State + data class Creating( + val parent: Path, + val name: String, + val isValid: Boolean, + val type: FileType, + val isCreating: Boolean + ) : State { + val options: ImmutableList + get() = when (type) { + FileType.FILE -> FileManagerConstants.FILE_EXTENSION_HINTS + + FileType.DIR -> emptyList() + }.toImmutableList() + + val needShowOptions: Boolean + get() = !name.contains(".") && options.isNotEmpty() + } + } + + sealed interface Event { + data class Created(val item: ListingItem) : Event + } +} diff --git a/components/filemngr/editor/impl/build.gradle.kts b/components/filemngr/editor/impl/build.gradle.kts index e3a72ae0fc..f6c8fe2d81 100644 --- a/components/filemngr/editor/impl/build.gradle.kts +++ b/components/filemngr/editor/impl/build.gradle.kts @@ -36,6 +36,7 @@ commonDependencies { implementation(projects.components.filemngr.editor.api) implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.main.api) + implementation(projects.components.filemngr.util) // Compose implementation(libs.compose.ui) diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/dialog/CreateFileDialogComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/dialog/CreateFileDialogComposable.kt index 76fe44735e..9fd7c2e2ef 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/dialog/CreateFileDialogComposable.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/dialog/CreateFileDialogComposable.kt @@ -5,14 +5,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.flipperdevices.filemanager.editor.viewmodel.FileNameViewModel import com.flipperdevices.filemanager.ui.components.name.NameDialog +import com.flipperdevices.filemanager.util.constant.FileManagerConstants import flipperapp.components.filemngr.editor.impl.generated.resources.fme_save_as_dialog_button import flipperapp.components.filemngr.editor.impl.generated.resources.fme_save_as_dialog_chars import flipperapp.components.filemngr.editor.impl.generated.resources.fme_save_as_dialog_title import org.jetbrains.compose.resources.stringResource import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME -private const val AVAILABLE_CHARACTERS = "“0-9”, “A-Z”, “a-z”, “!#\\\$%&'()-@^_`{}~”" - @Composable fun CreateFileDialogComposable( fileNameViewModel: FileNameViewModel, @@ -27,7 +26,7 @@ fun CreateFileDialogComposable( buttonText = stringResource(FME.string.fme_save_as_dialog_button), subtitle = stringResource( resource = FME.string.fme_save_as_dialog_chars, - AVAILABLE_CHARACTERS + FileManagerConstants.FILE_NAME_AVAILABLE_CHARACTERS ), onFinish = { onFinish(localState.name) diff --git a/components/filemngr/listing/impl/build.gradle.kts b/components/filemngr/listing/impl/build.gradle.kts index 57d367d198..1cf261510d 100644 --- a/components/filemngr/listing/impl/build.gradle.kts +++ b/components/filemngr/listing/impl/build.gradle.kts @@ -33,6 +33,9 @@ commonDependencies { implementation(projects.components.filemngr.main.api) implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.download.api) + implementation(projects.components.filemngr.rename.api) + implementation(projects.components.filemngr.create.api) + implementation(projects.components.filemngr.util) // Compose implementation(libs.compose.ui) diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt index 273a08e4a9..2d84975c18 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/api/FilesDecomposeComponentImpl.kt @@ -1,6 +1,7 @@ package com.flipperdevices.filemanager.listing.impl.api import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.childContext import com.arkivanov.decompose.router.slot.ChildSlot @@ -12,6 +13,7 @@ import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.instancekeeper.getOrCreate import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory +import com.flipperdevices.filemanager.create.api.CreateFileDecomposeComponent import com.flipperdevices.filemanager.download.api.DownloadDecomposeComponent import com.flipperdevices.filemanager.listing.api.FilesDecomposeComponent import com.flipperdevices.filemanager.listing.impl.composable.ComposableFileListScreen @@ -19,11 +21,11 @@ import com.flipperdevices.filemanager.listing.impl.composable.LaunchedEventsComp import com.flipperdevices.filemanager.listing.impl.composable.modal.FileOptionsBottomSheet import com.flipperdevices.filemanager.listing.impl.model.PathWithType import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel 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 com.flipperdevices.filemanager.listing.impl.viewmodel.StorageInfoViewModel +import com.flipperdevices.filemanager.rename.api.RenameDecomposeComponent import com.flipperdevices.filemanager.upload.api.UploadDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import dagger.assisted.Assisted @@ -43,15 +45,17 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( @Assisted private val searchCallback: SearchCallback, private val storageInfoViewModelFactory: Provider, private val optionsInfoViewModelFactory: Provider, - private val editFileViewModelFactory: Provider, private val deleteFilesViewModelFactory: Provider, private val filesViewModelFactory: FilesViewModel.Factory, private val downloadDecomposeComponentFactory: DownloadDecomposeComponent.Factory, private val createSelectionViewModel: Provider, private val uploadDecomposeComponentFactory: UploadDecomposeComponent.Factory, + private val renameDecomposeComponentFactory: RenameDecomposeComponent.Factory, + private val createFileDecomposeComponentFactory: CreateFileDecomposeComponent.Factory, ) : FilesDecomposeComponent(componentContext) { private val slotNavigation = SlotNavigation() + val fileOptionsSlot: Value> = childSlot( source = slotNavigation, handleBackButton = true, @@ -79,6 +83,24 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( ) } + private val renameDecomposeComponent by lazy { + renameDecomposeComponentFactory.invoke( + componentContext = childContext("FilesDecomposeComponent_renameDecomposeComponent"), + renamedCallback = { oldFullPath, newFullPath -> + filesViewModel.fileRenamed(oldFullPath, newFullPath) + } + ) + } + + private val createDecomposeComponent by lazy { + createFileDecomposeComponentFactory.invoke( + componentContext = childContext("FilesDecomposeComponent_createDecomposeComponent"), + createCallback = { item -> + filesViewModel.onFilesChanged(listOf(item)) + } + ) + } + private val backCallback = BackCallback { val parent = path.parent when { @@ -113,21 +135,15 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( val optionsViewModel = viewModelWithFactory(path.root.toString()) { optionsInfoViewModelFactory.get() } - val createFileViewModel = viewModelWithFactory(path.root.toString()) { - editFileViewModelFactory.get() - } val deleteFileViewModel = viewModelWithFactory(path.toString()) { deleteFilesViewModelFactory.get() } LaunchedEventsComposable( - editFileViewModel = createFileViewModel, deleteFilesViewModel = deleteFileViewModel, onFileRemove = filesViewModel::fileDeleted, - onFileListChange = filesViewModel::tryListFiles ) ComposableFileListScreen( path = path, - editFileViewModel = createFileViewModel, deleteFileViewModel = deleteFileViewModel, filesViewModel = filesViewModel, optionsViewModel = optionsViewModel, @@ -138,17 +154,30 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( onPathChange = pathChangedCallback::invoke, onFileMoreClick = slotNavigation::activate, onSearchClick = searchCallback::invoke, - onEditFileClick = fileSelectedCallback::invoke + onEditFileClick = fileSelectedCallback::invoke, + onRename = { pathWithType -> + renameDecomposeComponent.startRename(pathWithType.fullPath, pathWithType.fileType) + }, + canCreateFiles = createDecomposeComponent.canCreateFiles + .collectAsState() + .value, + onCreate = { type -> + createDecomposeComponent.startCreate(path, type) + } ) FileOptionsBottomSheet( fileOptionsSlot = fileOptionsSlot, slotNavigation = slotNavigation, selectionViewModel = selectionViewModel, - createFileViewModel = createFileViewModel, deleteFileViewModel = deleteFileViewModel, - onDownloadFile = downloadDecomposeComponent::download + onDownloadFile = downloadDecomposeComponent::download, + onRename = { pathWithType -> + renameDecomposeComponent.startRename(pathWithType.fullPath, pathWithType.fileType) + } ) uploadDecomposeComponent.Render() downloadDecomposeComponent.Render() + renameDecomposeComponent.Render() + createDecomposeComponent.Render() } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt index 7a51faf2ae..f924153377 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/ComposableFileListScreen.kt @@ -12,14 +12,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType import com.flipperdevices.core.preference.pb.FileManagerOrientation import com.flipperdevices.filemanager.listing.impl.composable.appbar.FileListAppBar -import com.flipperdevices.filemanager.listing.impl.composable.dialog.CreateFileDialogComposable import com.flipperdevices.filemanager.listing.impl.composable.dialog.DeleteFileDialog import com.flipperdevices.filemanager.listing.impl.composable.options.FullScreenBottomBarOptions import com.flipperdevices.filemanager.listing.impl.model.PathWithType import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.OptionsViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.SelectionViewModel @@ -30,7 +29,7 @@ import okio.Path @Composable fun ComposableFileListScreen( path: Path, - editFileViewModel: EditFileViewModel, + canCreateFiles: Boolean, deleteFileViewModel: DeleteFilesViewModel, filesViewModel: FilesViewModel, optionsViewModel: OptionsViewModel, @@ -42,9 +41,10 @@ fun ComposableFileListScreen( onPathChange: (Path) -> Unit, onEditFileClick: (Path) -> Unit, onFileMoreClick: (PathWithType) -> Unit, + onCreate: (FileType) -> Unit, + onRename: (PathWithType) -> Unit, modifier: Modifier = Modifier ) { - val canCreateFiles by editFileViewModel.canCreateFiles.collectAsState() val canDeleteFiles by deleteFileViewModel.canDeleteFiles.collectAsState() val filesListState by filesViewModel.state.collectAsState() val optionsState by optionsViewModel.state.collectAsState() @@ -60,17 +60,14 @@ fun ComposableFileListScreen( filesListState = filesListState, optionsState = optionsState, optionsViewModel = optionsViewModel, - canCreateFiles = canCreateFiles, onUploadClick = onUploadClick, - editFileViewModel = editFileViewModel, onBack = onBack, - onSearchClick = onSearchClick + onSearchClick = onSearchClick, + onCreate = onCreate, + canCreateFiles = canCreateFiles ) } ) { contentPadding -> - CreateFileDialogComposable( - editFileViewModel = editFileViewModel, - ) DeleteFileDialog( deleteFileState = deleteFileState, deleteFileViewModel = deleteFileViewModel @@ -131,8 +128,8 @@ fun ComposableFileListScreen( selectionState = selectionState, filesListState = filesListState, selectionViewModel = selectionViewModel, - editFileViewModel = editFileViewModel, deleteFileViewModel = deleteFileViewModel, + onRename = onRename ) } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LaunchedEventsComposable.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LaunchedEventsComposable.kt index 162909995f..84325abfb5 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LaunchedEventsComposable.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LaunchedEventsComposable.kt @@ -3,26 +3,16 @@ package com.flipperdevices.filemanager.listing.impl.composable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import okio.Path @Composable fun LaunchedEventsComposable( - editFileViewModel: EditFileViewModel, deleteFilesViewModel: DeleteFilesViewModel, - onFileListChange: () -> Unit, onFileRemove: (Path) -> Unit ) { - LaunchedEffect(editFileViewModel, deleteFilesViewModel) { - editFileViewModel.event.onEach { - when (it) { - EditFileViewModel.Event.FilesChanged -> { - onFileListChange.invoke() - } - } - }.launchIn(this) + LaunchedEffect(deleteFilesViewModel) { deleteFilesViewModel.event.onEach { when (it) { DeleteFilesViewModel.Event.CouldNotDeleteSomeFiles -> Unit diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt index dc1ad58d5f..65ad1fbbcd 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/appbar/FileListAppBar.kt @@ -22,7 +22,6 @@ import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.filemanager.listing.impl.composable.MoreIconComposable import com.flipperdevices.filemanager.listing.impl.model.PathWithType -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.OptionsViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.SelectionViewModel @@ -46,7 +45,7 @@ fun FileListAppBar( canCreateFiles: Boolean, onUploadClick: () -> Unit, onSearchClick: () -> Unit, - editFileViewModel: EditFileViewModel, + onCreate: (FileType) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier ) { @@ -101,10 +100,10 @@ fun FileListAppBar( onUploadClick = onUploadClick, onSelectClick = selectionViewModel::toggleMode, onCreateFolderClick = { - editFileViewModel.onCreate(path, FileType.DIR) + onCreate.invoke(FileType.DIR) }, onCreateFileClick = { - editFileViewModel.onCreate(path, FileType.FILE) + onCreate.invoke(FileType.FILE) } ) } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/dialog/CreateFileDialogComposable.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/dialog/CreateFileDialogComposable.kt deleted file mode 100644 index 57af38ca59..0000000000 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/dialog/CreateFileDialogComposable.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.flipperdevices.filemanager.listing.impl.composable.dialog - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel -import com.flipperdevices.filemanager.ui.components.name.NameDialog -import flipperapp.components.filemngr.listing.impl.generated.resources.fml_create_file_allowed_chars -import flipperapp.components.filemngr.listing.impl.generated.resources.fml_create_file_file_btn -import flipperapp.components.filemngr.listing.impl.generated.resources.fml_create_file_folder_btn -import flipperapp.components.filemngr.listing.impl.generated.resources.fml_create_file_title -import org.jetbrains.compose.resources.stringResource -import flipperapp.components.filemngr.listing.impl.generated.resources.Res as FML - -private const val AVAILABLE_CHARACTERS = "“0-9”, “A-Z”, “a-z”, “!#\\\$%&'()-@^_`{}~”" - -@Composable -fun CreateFileDialogComposable( - editFileViewModel: EditFileViewModel, -) { - val createFileState by editFileViewModel.state.collectAsState() - - when (val localCreateFileState = createFileState) { - EditFileViewModel.State.Pending -> Unit - is EditFileViewModel.State.Edit -> { - NameDialog( - value = localCreateFileState.name, - title = stringResource(FML.string.fml_create_file_title), - buttonText = stringResource( - resource = when (localCreateFileState.itemType) { - FileType.FILE -> FML.string.fml_create_file_file_btn - FileType.DIR -> FML.string.fml_create_file_folder_btn - } - ), - subtitle = stringResource( - FML.string.fml_create_file_allowed_chars, - AVAILABLE_CHARACTERS - ), - onFinish = { editFileViewModel.onFinish() }, - isError = !localCreateFileState.isValid, - isEnabled = !localCreateFileState.isLoading, - needShowOptions = localCreateFileState.needShowOptions, - onTextChange = editFileViewModel::onNameChange, - onDismissRequest = editFileViewModel::dismiss, - onOptionSelect = editFileViewModel::onOptionSelected, - options = localCreateFileState.options - ) - } - } -} diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/BottomSheetOptionsContent.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/BottomSheetOptionsContent.kt index 79373ec86b..1d62ca1a85 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/BottomSheetOptionsContent.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/BottomSheetOptionsContent.kt @@ -100,14 +100,14 @@ fun BottomSheetOptionsContent( text = stringResource(FML.string.fml_export), painter = painterResource(FR.drawable.ic_upload), onClick = onExport, - isEnabled = fileType == FileType.FILE + isEnabled = true ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_rename), painter = painterResource(FR.drawable.ic_edit), onClick = onRename, - isEnabled = fileType == FileType.FILE + isEnabled = true ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/FileOptionsBottomSheet.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/FileOptionsBottomSheet.kt index f8034acb50..566c78b476 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/FileOptionsBottomSheet.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/FileOptionsBottomSheet.kt @@ -9,18 +9,17 @@ import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.value.Value import com.flipperdevices.filemanager.listing.impl.model.PathWithType import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.SelectionViewModel import okio.Path @Composable fun FileOptionsBottomSheet( - createFileViewModel: EditFileViewModel, fileOptionsSlot: Value>, slotNavigation: SlotNavigation, selectionViewModel: SelectionViewModel, deleteFileViewModel: DeleteFilesViewModel, onDownloadFile: (Path, Long) -> Unit, + onRename: (PathWithType) -> Unit, modifier: Modifier = Modifier ) { SlotModalBottomSheet( @@ -37,7 +36,7 @@ fun FileOptionsBottomSheet( slotNavigation.dismiss() }, onRename = { - createFileViewModel.onRename(pathWithType) + onRename.invoke(pathWithType) slotNavigation.dismiss() }, onExport = { diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt index d964f2360e..1e8a276be7 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptions.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.filemanager.listing.impl.model.PathWithType import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel -import com.flipperdevices.filemanager.listing.impl.viewmodel.EditFileViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.SelectionViewModel import com.flipperdevices.filemanager.ui.components.dropdown.IconDropdownItem @@ -138,10 +137,10 @@ fun BottomBarOptions( @Composable fun FullScreenBottomBarOptions( deleteFileViewModel: DeleteFilesViewModel, - editFileViewModel: EditFileViewModel, selectionViewModel: SelectionViewModel, filesListState: FilesViewModel.State, selectionState: SelectionViewModel.State, + onRename: (PathWithType) -> Unit, modifier: Modifier = Modifier ) { Box( @@ -159,9 +158,9 @@ fun FullScreenBottomBarOptions( canRename = selectionState.canRename, onMove = {}, // todo onRename = { - val path = selectionState.selected.firstOrNull() ?: return@BottomBarOptions + val pathWithType = selectionState.selected.firstOrNull() ?: return@BottomBarOptions selectionViewModel.toggleMode() - editFileViewModel.onRename(path) + onRename.invoke(pathWithType) }, onDelete = { deleteFileViewModel.tryDelete(selectionState.selected.map(PathWithType::fullPath)) diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/PathWithType.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/PathWithType.kt index c64c772919..59fc27a1ca 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/PathWithType.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/PathWithType.kt @@ -1,7 +1,7 @@ package com.flipperdevices.filemanager.listing.impl.model import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType -import com.flipperdevices.filemanager.main.serialization.PathSerializer +import com.flipperdevices.filemanager.util.serialization.PathSerializer import kotlinx.serialization.Serializable import okio.Path diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/EditFileViewModel.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/EditFileViewModel.kt deleted file mode 100644 index dd8207badf..0000000000 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/EditFileViewModel.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.flipperdevices.filemanager.listing.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.FFileUploadApi -import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType -import com.flipperdevices.bridge.connection.feature.storage.api.model.StorageRequestPriority -import com.flipperdevices.bridge.dao.api.model.FlipperKeyType -import com.flipperdevices.core.ktx.jre.FlipperFileNameValidator -import com.flipperdevices.core.ktx.jre.launchWithLock -import com.flipperdevices.core.log.LogTagProvider -import com.flipperdevices.core.log.error -import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import com.flipperdevices.filemanager.listing.impl.model.PathWithType -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.sync.Mutex -import okio.ByteString -import okio.Path -import okio.buffer -import javax.inject.Inject - -class EditFileViewModel @Inject constructor( - featureProvider: FFeatureProvider, -) : DecomposeViewModel(), LogTagProvider { - override val TAG: String = "CreateFolderViewModel" - - private val channel = Channel() - val event = channel.receiveAsFlow() - - private val fileNameValidator = FlipperFileNameValidator() - private val mutex = Mutex() - - private val _state = MutableStateFlow(State.Pending) - val state = _state.asStateFlow() - - fun onCreate(path: Path, fileType: FileType) { - _state.update { - State.Edit.Create( - name = "", - itemType = fileType, - path = path, - isValid = false - ) - } - } - - fun onRename(pathWithType: PathWithType) { - _state.update { - State.Edit.Rename( - name = pathWithType.fullPath.name, - itemType = pathWithType.fileType, - fullPath = pathWithType.fullPath, - isValid = true - ) - } - } - - fun dismiss() { - _state.value = State.Pending - } - - fun onNameChange(name: String) { - val visibleState = state.value as? State.Edit ?: return - _state.value = visibleState.with(name = name) - } - - fun onOptionSelected(index: Int) { - val visibleState = state.value as? State.Edit ?: return - val option = visibleState.options.getOrNull(index) ?: return - _state.update { visibleState.with(name = option) } - } - - private val featureState = featureProvider.get() - .stateIn(viewModelScope, SharingStarted.Eagerly, FFeatureStatus.Retrieving) - - val canCreateFiles = featureState - .filterIsInstance>() - .map { true } - .stateIn(viewModelScope, SharingStarted.Eagerly, false) - - private suspend fun uploadFolder(uploadApi: FFileUploadApi, pathOnFlipper: Path) { - uploadApi.mkdir(pathOnFlipper.toString()) - .onSuccess { channel.send(Event.FilesChanged) } - .onFailure { error(it) { "Could not create folder" } } - } - - private suspend fun uploadFile(uploadApi: FFileUploadApi, pathOnFlipper: Path) { - runCatching { - uploadApi.sink( - pathOnFlipper = pathOnFlipper.toString(), - priority = StorageRequestPriority.FOREGROUND - ).buffer().use { it.write(ByteString.of()) } - }.onSuccess { channel.send(Event.FilesChanged) } - .onFailure { error(it) { "Could not create file" } } - } - - fun onFinish() { - val state = state.value as? State.Edit ?: return - if (!state.isValid) return - if (!canCreateFiles.value) return - launchWithLock(mutex, viewModelScope, "create folder") { - _state.emit(state.with(isLoading = true)) - val storageApi = featureState - .filterIsInstance>() - .first() - .featureApi - - val uploadApi = storageApi.uploadApi() - when (state) { - is State.Edit.Create -> { - val pathOnFlipper = state.path.resolve(state.name) - when (state.itemType) { - FileType.FILE -> uploadFile( - uploadApi = uploadApi, - pathOnFlipper = pathOnFlipper - ) - - FileType.DIR -> uploadFolder( - uploadApi = uploadApi, - pathOnFlipper = pathOnFlipper - ) - } - } - - is State.Edit.Rename -> { - val pathOnFlipper = state.fullPath.parent?.resolve(state.name) ?: run { - error { "#onFinish could not move file because parent is null ${state.fullPath}" } - return@launchWithLock - } - // todo folders doesn't rename - uploadApi.move( - oldPath = state.fullPath, - newPath = pathOnFlipper - ).onSuccess { channel.send(Event.FilesChanged) } - .onFailure { error(it) { "#onFinish could not move file ${state.fullPath} -> $pathOnFlipper" } } - } - } - - _state.emit(State.Pending) - } - } - - init { - state - .filterIsInstance() - .distinctUntilChangedBy { state -> state.name } - .onEach { state -> - _state.emit(state.with(isValid = fileNameValidator.isValid(state.name))) - }.launchIn(viewModelScope) - } - - sealed interface State { - data object Pending : State - - sealed interface Edit : State { - val name: String - val isValid: Boolean - val itemType: FileType - val isLoading: Boolean - - fun with( - name: String = this.name, - isValid: Boolean = this.isValid, - isLoading: Boolean = this.isLoading - ): Edit - - data class Create( - val path: Path, - override val name: String = "", - override val isValid: Boolean = false, - override val itemType: FileType, - override val isLoading: Boolean = false, - ) : Edit { - override fun with(name: String, isValid: Boolean, isLoading: Boolean): Create { - return copy( - name = name, - isValid = isValid, - isLoading = isLoading - ) - } - } - - data class Rename( - val fullPath: Path, - override val name: String = "", - override val isValid: Boolean = false, - override val itemType: FileType, - override val isLoading: Boolean = false - ) : Edit { - override fun with(name: String, isValid: Boolean, isLoading: Boolean): Rename { - return copy( - name = name, - isValid = isValid, - isLoading = isLoading - ) - } - } - - val options - get() = when (itemType) { - FileType.FILE -> listOf("txt") - .plus(FlipperKeyType.entries.map { it.extension }) - .map { extension -> "$name.$extension" } - - FileType.DIR -> emptyList() - }.toImmutableList() - - val needShowOptions - get() = !name.contains(".") && options.isNotEmpty() - } - } - - sealed interface Event { - data object FilesChanged : Event - } -} diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/FilesViewModel.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/FilesViewModel.kt index d1e3179262..1b3611ac85 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/FilesViewModel.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/FilesViewModel.kt @@ -168,6 +168,21 @@ class FilesViewModel @AssistedInject constructor( } } + fun fileRenamed(oldPath: Path, newPath: Path) { + _state.update { state -> + (state as? State.Loaded)?.let { loadedState -> + val mutableFiles = loadedState.files.toMutableList() + val i = mutableFiles.indexOfFirst { item -> item.path.name == oldPath.name } + if (i == -1) return@let loadedState + mutableFiles[i] = when (val item = mutableFiles[i]) { + is ExtendedListingItem.File -> item.copy(path = newPath.name.toPath()) + is ExtendedListingItem.Folder -> item.copy(path = newPath.name.toPath()) + } + loadedState.copy(files = mutableFiles.toImmutableList()) + } ?: state + } + } + fun tryListFiles() { launchWithLock(mutex, viewModelScope, "try_list_files") { _state.emit(State.Loading) diff --git a/components/filemngr/main/impl/build.gradle.kts b/components/filemngr/main/impl/build.gradle.kts index f6f5451bea..55b2c288c1 100644 --- a/components/filemngr/main/impl/build.gradle.kts +++ b/components/filemngr/main/impl/build.gradle.kts @@ -25,7 +25,7 @@ commonDependencies { implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.search.api) implementation(projects.components.filemngr.editor.api) - implementation(projects.components.newfilemanager.api) + implementation(projects.components.filemngr.util) // Compose implementation(libs.compose.ui) diff --git a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt index d98b19bc90..9e368eadc6 100644 --- a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt +++ b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/model/FileManagerNavigationConfig.kt @@ -1,7 +1,7 @@ package com.flipperdevices.filemanager.main.impl.model import androidx.compose.runtime.Stable -import com.flipperdevices.filemanager.main.serialization.PathSerializer +import com.flipperdevices.filemanager.util.serialization.PathSerializer import kotlinx.serialization.Serializable import okio.Path import okio.Path.Companion.toPath diff --git a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/serialization/PathSerializer.kt b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/serialization/PathSerializer.kt deleted file mode 100644 index fecf366f75..0000000000 --- a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/serialization/PathSerializer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.flipperdevices.filemanager.main.impl.serialization - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import okio.Path -import okio.Path.Companion.toPath - -object PathSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor( - serialName = "okio.Path", - kind = PrimitiveKind.STRING - ) - - override fun deserialize(decoder: Decoder): Path { - val path = decoder.decodeString() - return path.toPath() - } - - override fun serialize(encoder: Encoder, value: Path) { - encoder.encodeString(value.toString()) - } -} diff --git a/components/filemngr/rename/api/build.gradle.kts b/components/filemngr/rename/api/build.gradle.kts new file mode 100644 index 0000000000..70ff8d0d93 --- /dev/null +++ b/components/filemngr/rename/api/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") +} + +android.namespace = "com.flipperdevices.filemanager.rename.api" + +commonDependencies { + implementation(projects.components.bridge.connection.feature.storage.api) + + implementation(projects.components.core.ui.decompose) + + implementation(libs.compose.ui) + implementation(libs.decompose) + + implementation(libs.okio) +} diff --git a/components/filemngr/rename/api/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/api/RenameDecomposeComponent.kt b/components/filemngr/rename/api/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/api/RenameDecomposeComponent.kt new file mode 100644 index 0000000000..ef510262ad --- /dev/null +++ b/components/filemngr/rename/api/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/api/RenameDecomposeComponent.kt @@ -0,0 +1,24 @@ +package com.flipperdevices.filemanager.rename.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import okio.Path + +abstract class RenameDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + + abstract fun startRename(fullPath: Path, type: FileType) + + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + renamedCallback: RenamedCallback, + ): RenameDecomposeComponent + } + + fun interface RenamedCallback { + fun invoke(oldFullPath: Path, newFullPath: Path) + } +} diff --git a/components/filemngr/rename/impl/build.gradle.kts b/components/filemngr/rename/impl/build.gradle.kts new file mode 100644 index 0000000000..1be48b0b3d --- /dev/null +++ b/components/filemngr/rename/impl/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.filemanager.rename.impl" + +commonDependencies { + 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.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.util) + implementation(projects.components.filemngr.uiComponents) + implementation(projects.components.filemngr.rename.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + + 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) +} diff --git a/components/filemngr/rename/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/rename/impl/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..d2c65a2181 --- /dev/null +++ b/components/filemngr/rename/impl/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,6 @@ + + + Enter Name: + Save + Allowed characters: %1$s + \ No newline at end of file diff --git a/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/api/RenameDecomposeComponentImpl.kt b/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/api/RenameDecomposeComponentImpl.kt new file mode 100644 index 0000000000..658566b7bc --- /dev/null +++ b/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/api/RenameDecomposeComponentImpl.kt @@ -0,0 +1,78 @@ +package com.flipperdevices.filemanager.rename.impl.api + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.filemanager.rename.api.RenameDecomposeComponent +import com.flipperdevices.filemanager.rename.impl.viewmodel.RenameViewModel +import com.flipperdevices.filemanager.ui.components.name.NameDialog +import com.flipperdevices.filemanager.util.constant.FileManagerConstants +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import flipperapp.components.filemngr.rename.impl.generated.resources.fmr_create_file_allowed_chars +import flipperapp.components.filemngr.rename.impl.generated.resources.fmr_create_file_folder_btn +import flipperapp.components.filemngr.rename.impl.generated.resources.fmr_create_file_title +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.gulya.anvil.assisted.ContributesAssistedFactory +import okio.Path +import org.jetbrains.compose.resources.stringResource +import javax.inject.Provider +import flipperapp.components.filemngr.rename.impl.generated.resources.Res as FMR + +@ContributesAssistedFactory(AppGraph::class, RenameDecomposeComponent.Factory::class) +class RenameDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted val renamedCallback: RenamedCallback, + renameViewModelProvider: Provider +) : RenameDecomposeComponent(componentContext) { + private val renameViewModel = instanceKeeper.getOrCreate { + renameViewModelProvider.get() + } + + override fun startRename(fullPath: Path, type: FileType) { + renameViewModel.startRename(fullPath, type) + } + + @Composable + override fun Render() { + val state by renameViewModel.state.collectAsState() + LaunchedEffect(renameViewModel) { + renameViewModel.event + .onEach { event -> + when (event) { + is RenameViewModel.Event.Renamed -> { + renamedCallback.invoke(event.oldFullPath, event.newFullPath) + } + } + }.launchIn(this) + } + when (val localState = state) { + RenameViewModel.State.Pending -> Unit + is RenameViewModel.State.Renaming -> { + NameDialog( + value = localState.name, + title = stringResource(FMR.string.fmr_create_file_title), + buttonText = stringResource(FMR.string.fmr_create_file_folder_btn), + subtitle = stringResource( + FMR.string.fmr_create_file_allowed_chars, + FileManagerConstants.FILE_NAME_AVAILABLE_CHARACTERS + ), + onFinish = renameViewModel::onConfirm, + isError = !localState.isValid, + isEnabled = !localState.isRenaming, + needShowOptions = localState.needShowOptions, + onTextChange = renameViewModel::onNameChange, + onDismissRequest = renameViewModel::dismiss, + onOptionSelect = renameViewModel::onOptionSelected, + options = localState.options + ) + } + } + } +} diff --git a/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/viewmodel/RenameViewModel.kt b/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/viewmodel/RenameViewModel.kt new file mode 100644 index 0000000000..3966368339 --- /dev/null +++ b/components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/viewmodel/RenameViewModel.kt @@ -0,0 +1,176 @@ +package com.flipperdevices.filemanager.rename.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.FFileUploadApi +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.core.ktx.jre.FlipperFileNameValidator +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.filemanager.util.constant.FileManagerConstants +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okio.Path +import javax.inject.Inject + +class RenameViewModel @Inject constructor( + private val featureProvider: FFeatureProvider, +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "RenameViewModel" + + private val fileNameValidator = FlipperFileNameValidator() + + private val eventChannel = Channel() + val event = eventChannel.receiveAsFlow() + + private val _state = MutableStateFlow(State.Pending) + val state = _state.asStateFlow() + + private var featureJob: Job? = null + private val featureMutex = Mutex() + + val canRenameFiles = featureProvider.get() + .filterIsInstance>() + .map { true } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + fun startRename(fullPath: Path, type: FileType) { + _state.update { + State.Renaming( + fullPath = fullPath, + type = type, + name = fullPath.name, + isValid = false, + isRenaming = false + ) + } + } + + fun dismiss() { + _state.update { State.Pending } + } + + fun onNameChange(name: String) { + _state.update { state -> + (state as? State.Renaming) + ?.copy(name = name) + ?: state + } + } + + fun onOptionSelected(index: Int) { + _state.update { state -> + (state as? State.Renaming)?.let { renamingState -> + val option = renamingState.options.getOrNull(index).orEmpty() + renamingState.copy(name = "${renamingState.name}.$option") + } ?: state + } + } + + private suspend fun rename(uploadApi: FFileUploadApi) { + val state = (state.first() as? State.Renaming) + if (state == null) { + error { "#rename state was not Renaming" } + return + } + val newPath = state.fullPath.parent?.resolve(state.name) + if (newPath == null) { + error { "#rename parent path was null for ${state.fullPath}" } + return + } + _state.emit(state.copy(isRenaming = true)) + uploadApi.move( + oldPath = state.fullPath, + newPath = newPath + ).onSuccess { + val event = Event.Renamed( + oldFullPath = state.fullPath, + newFullPath = newPath + ) + eventChannel.send(event) + }.onFailure { + error(it) { "#onFinish could not move file ${state.fullPath} -> $newPath" } + } + _state.emit(State.Pending) + featureJob?.cancelAndJoin() + } + + fun onConfirm() { + viewModelScope.launch { + featureJob?.cancelAndJoin() + featureMutex.withLock { + featureJob = featureProvider.get() + .onEach { status -> + when (status) { + FFeatureStatus.NotFound -> Unit + FFeatureStatus.Retrieving -> Unit + FFeatureStatus.Unsupported -> Unit + is FFeatureStatus.Supported -> { + rename(status.featureApi.uploadApi()) + } + } + }.launchIn(viewModelScope) + featureJob?.join() + } + } + } + + private fun collectNameValidation() { + state + .filterIsInstance() + .distinctUntilChangedBy { state -> state.name } + .onEach { state -> + _state.emit(state.copy(isValid = fileNameValidator.isValid(state.name))) + }.launchIn(viewModelScope) + } + + init { + collectNameValidation() + } + + sealed interface State { + data object Pending : State + data class Renaming( + val fullPath: Path, + val name: String, + val isValid: Boolean, + val type: FileType, + val isRenaming: Boolean + ) : State { + val options: ImmutableList + get() = when (type) { + FileType.FILE -> FileManagerConstants.FILE_EXTENSION_HINTS + + FileType.DIR -> emptyList() + }.toImmutableList() + + val needShowOptions: Boolean + get() = !name.contains(".") && options.isNotEmpty() + } + } + + sealed interface Event { + data class Renamed(val oldFullPath: Path, val newFullPath: Path) : Event + } +} diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/AutoCompleteTextField.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/AutoCompleteTextField.kt index 748370cad0..b850854d2d 100644 --- a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/AutoCompleteTextField.kt +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/AutoCompleteTextField.kt @@ -35,6 +35,7 @@ internal fun AutoCompleteTextField( onOptionSelect: (index: Int) -> Unit, modifier: Modifier = Modifier, needShowOptions: Boolean = true, + isEnabled: Boolean = true, ) { var isExpanded by rememberSaveable { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } @@ -50,14 +51,15 @@ internal fun AutoCompleteTextField( onTextChange = onTextChange, interactionSource = interactionSource, modifier = Modifier.onFocusEvent { isExpanded = it.isFocused }, - isError = isError + isError = isError, + enabled = isEnabled ) DropdownMenu( modifier = Modifier .heightIn(max = 152.dp) .wrapContentWidth(), - expanded = isExpanded && needShowOptions && options.isNotEmpty(), + expanded = isExpanded && needShowOptions && options.isNotEmpty() && isEnabled, onDismissRequest = { }, properties = PopupProperties(focusable = false) ) { diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialog.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialog.kt index 35449fd38d..eabecf68a5 100644 --- a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialog.kt +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialog.kt @@ -74,7 +74,8 @@ fun NameDialog( needShowOptions = needShowOptions, title = title, subtitle = subtitle, - isError = isError + isError = isError, + isEnabled = isEnabled ) Spacer(Modifier.height(24.dp)) diff --git a/components/filemngr/util/build.gradle.kts b/components/filemngr/util/build.gradle.kts new file mode 100644 index 0000000000..22e9e84584 --- /dev/null +++ b/components/filemngr/util/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") + id("kotlinx-serialization") +} + +android.namespace = "com.flipperdevices.filemanager.util" + +commonDependencies { + implementation(projects.components.bridge.dao.api) + + implementation(libs.kotlin.serialization.json) + + implementation(libs.okio) +} diff --git a/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/constant/FileManagerConstants.kt b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/constant/FileManagerConstants.kt new file mode 100644 index 0000000000..40963223cc --- /dev/null +++ b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/constant/FileManagerConstants.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.filemanager.util.constant + +import com.flipperdevices.bridge.dao.api.model.FlipperKeyType + +object FileManagerConstants { + const val FILE_NAME_AVAILABLE_CHARACTERS = "“0-9”, “A-Z”, “a-z”, “!#\\\$%&'()-@^_`{}~”" + + val FILE_EXTENSION_HINTS = listOf("txt").plus(FlipperKeyType.entries.map { it.extension }) +} diff --git a/components/filemngr/main/api/src/commonMain/kotlin/com/flipperdevices/filemanager/main/serialization/PathSerializer.kt b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt similarity index 93% rename from components/filemngr/main/api/src/commonMain/kotlin/com/flipperdevices/filemanager/main/serialization/PathSerializer.kt rename to components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt index d9eabc4258..017237d0de 100644 --- a/components/filemngr/main/api/src/commonMain/kotlin/com/flipperdevices/filemanager/main/serialization/PathSerializer.kt +++ b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.filemanager.main.serialization +package com.flipperdevices.filemanager.util.serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind diff --git a/settings.gradle.kts b/settings.gradle.kts index bfae6156c8..e7fa5b123e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,6 +86,7 @@ include( ":components:bridge:connection:feature:serialspeed:api", ":components:bridge:connection:feature:serialspeed:impl", + ":components:filemngr:util", ":components:filemanager:api", ":components:filemanager:impl", ":components:newfilemanager:api", @@ -103,6 +104,10 @@ include( ":components:filemngr:editor:impl", ":components:filemngr:download:api", ":components:filemngr:download:impl", + ":components:filemngr:rename:api", + ":components:filemngr:rename:impl", + ":components:filemngr:create:api", + ":components:filemngr:create:impl", ":components:core:di", ":components:core:ktx",