From 8b2440a7b3cee25f83cd3799086646d7528313bb Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:13:39 +0300 Subject: [PATCH 1/4] Refactor rename and file creation modules (#995) **Background** Rename and file creation are located insided listing module. Later this features will be required during file transfer and file copy. **Changes** - Move file creation and folder creation into separate module **Test plan** - The behaviour should be the same as before --- CHANGELOG.md | 1 + .../bridge/connection/sample/build.gradle.kts | 4 + .../filemngr/create/api/build.gradle.kts | 17 ++ .../api/CreateFileDecomposeComponent.kt | 31 +++ .../filemngr/create/impl/build.gradle.kts | 52 ++++ .../composeResources/values/strings.xml | 7 + .../api/CreateFileDecomposeComponentImpl.kt | 92 +++++++ .../impl/viewmodel/CreateFileViewModel.kt | 200 +++++++++++++++ .../filemngr/editor/impl/build.gradle.kts | 1 + .../dialog/CreateFileDialogComposable.kt | 5 +- .../filemngr/listing/impl/build.gradle.kts | 3 + .../impl/api/FilesDecomposeComponentImpl.kt | 51 +++- .../composable/ComposableFileListScreen.kt | 19 +- .../composable/LaunchedEventsComposable.kt | 12 +- .../impl/composable/appbar/FileListAppBar.kt | 7 +- .../dialog/CreateFileDialogComposable.kt | 51 ---- .../modal/BottomSheetOptionsContent.kt | 4 +- .../modal/FileOptionsBottomSheet.kt | 5 +- .../composable/options/BottomBarOptions.kt | 7 +- .../listing/impl/model/PathWithType.kt | 2 +- .../impl/viewmodel/EditFileViewModel.kt | 231 ------------------ .../listing/impl/viewmodel/FilesViewModel.kt | 15 ++ .../filemngr/main/impl/build.gradle.kts | 2 +- .../impl/model/FileManagerNavigationConfig.kt | 2 +- .../main/impl/serialization/PathSerializer.kt | 26 -- .../filemngr/rename/api/build.gradle.kts | 17 ++ .../rename/api/RenameDecomposeComponent.kt | 24 ++ .../filemngr/rename/impl/build.gradle.kts | 52 ++++ .../composeResources/values/strings.xml | 6 + .../impl/api/RenameDecomposeComponentImpl.kt | 78 ++++++ .../rename/impl/viewmodel/RenameViewModel.kt | 176 +++++++++++++ .../components/name/AutoCompleteTextField.kt | 6 +- .../ui/components/name/NameDialog.kt | 3 +- components/filemngr/util/build.gradle.kts | 15 ++ .../util/constant/FileManagerConstants.kt | 9 + .../util}/serialization/PathSerializer.kt | 2 +- settings.gradle.kts | 5 + 37 files changed, 876 insertions(+), 364 deletions(-) create mode 100644 components/filemngr/create/api/build.gradle.kts create mode 100644 components/filemngr/create/api/src/commonMain/kotlin/com/flipperdevices/filemanager/create/api/CreateFileDecomposeComponent.kt create mode 100644 components/filemngr/create/impl/build.gradle.kts create mode 100644 components/filemngr/create/impl/src/commonMain/composeResources/values/strings.xml create mode 100644 components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/api/CreateFileDecomposeComponentImpl.kt create mode 100644 components/filemngr/create/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/create/impl/viewmodel/CreateFileViewModel.kt delete mode 100644 components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/dialog/CreateFileDialogComposable.kt delete mode 100644 components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/EditFileViewModel.kt delete mode 100644 components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/serialization/PathSerializer.kt create mode 100644 components/filemngr/rename/api/build.gradle.kts create mode 100644 components/filemngr/rename/api/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/api/RenameDecomposeComponent.kt create mode 100644 components/filemngr/rename/impl/build.gradle.kts create mode 100644 components/filemngr/rename/impl/src/commonMain/composeResources/values/strings.xml create mode 100644 components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/api/RenameDecomposeComponentImpl.kt create mode 100644 components/filemngr/rename/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/rename/impl/viewmodel/RenameViewModel.kt create mode 100644 components/filemngr/util/build.gradle.kts create mode 100644 components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/constant/FileManagerConstants.kt rename components/filemngr/{main/api/src/commonMain/kotlin/com/flipperdevices/filemanager/main => util/src/commonMain/kotlin/com/flipperdevices/filemanager/util}/serialization/PathSerializer.kt (93%) 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", From f234ff0043fe05cc06cf10f406db0af6b2719c5f Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Mon, 2 Dec 2024 21:42:29 +0300 Subject: [PATCH 2/4] Improve file edit screen (#996) **Background** File editor currently contains a lot of logic, which makes code reading harder. It can be improved by splitting it into separate decompose screen for each task: Loading, Editing, Uploading **Changes** - Split editor into multiple decompose screens **Test plan** - Open new editor. See new beautiful new loading screen with progress. - After it loaded try edit your file - After finish try save file or save file as new file. - See new file immediately appeared in listing with updated size --- CHANGELOG.md | 1 + .../api/DownloadDecomposeComponentImpl.kt | 2 - .../filemngr/editor/api/build.gradle.kts | 1 + .../FileManagerEditorDecomposeComponent.kt | 10 +- .../filemngr/editor/impl/build.gradle.kts | 1 - .../composeResources/values/strings.xml | 4 + .../api/EditFileNameDecomposeComponent.kt | 46 +++++ .../editor/api/EditorDecomposeComponent.kt | 102 ++++++++++ .../api/FileDownloadDecomposeComponent.kt | 97 +++++++++ ...FileManagerEditorDecomposeComponentImpl.kt | 128 +++++++----- .../api/UploadFileDecomposeComponent.kt | 109 ++++++++++ .../editor/composable/EditorDropdown.kt | 10 +- .../composable/FileManagerEditorComposable.kt | 58 ++---- .../composable/content/LoadedContent.kt | 9 +- .../composable/content/UploadingContent.kt | 38 ---- .../dialog/CreateFileDialogComposable.kt | 54 +++-- .../download/InProgressComposable.kt | 93 +++++++++ .../download/UploadingComposable.kt | 74 +++++++ .../model/FileManagerEditorConfiguration.kt | 30 +++ .../editor/viewmodel/DownloadViewModel.kt | 134 +++++++++++++ .../editor/viewmodel/EditFileNameViewModel.kt | 65 ++++++ .../editor/viewmodel/EditorViewModel.kt | 189 ++++++------------ .../editor/viewmodel/FileNameViewModel.kt | 74 ------- .../editor/viewmodel/UploadFileViewModel.kt | 132 ++++++++++++ .../filemngr/listing/api/build.gradle.kts | 1 + .../listing/api/FilesDecomposeComponent.kt | 3 + .../impl/api/FilesDecomposeComponentImpl.kt | 5 + .../filemngr/main/impl/build.gradle.kts | 1 + .../api/FileManagerDecomposeComponentImpl.kt | 7 + .../util/constant/FileManagerConstants.kt | 5 + 30 files changed, 1108 insertions(+), 375 deletions(-) create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditFileNameDecomposeComponent.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditorDecomposeComponent.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/UploadingContent.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/model/FileManagerEditorConfiguration.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/DownloadViewModel.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/FileNameViewModel.kt create mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/UploadFileViewModel.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 933e26366c..f7afc0244a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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 +- [Refactor] Improve and refactor new FileManager Editor - [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/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt index 7c4ada5d24..5f1cb33c1c 100644 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt @@ -13,7 +13,6 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope import com.flipperdevices.core.di.AppGraph -import com.flipperdevices.core.share.PlatformShareHelper import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.filemanager.download.api.DownloadDecomposeComponent import com.flipperdevices.filemanager.download.impl.composable.DownloadingComposable @@ -32,7 +31,6 @@ import javax.inject.Provider class DownloadDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, private val downloadViewModelFactory: Provider, - private val platformShareHelper: PlatformShareHelper ) : DownloadDecomposeComponent(componentContext) { private val downloadViewModel = instanceKeeper.getOrCreate { downloadViewModelFactory.get() diff --git a/components/filemngr/editor/api/build.gradle.kts b/components/filemngr/editor/api/build.gradle.kts index bbf7380e3a..64ec4e8235 100644 --- a/components/filemngr/editor/api/build.gradle.kts +++ b/components/filemngr/editor/api/build.gradle.kts @@ -7,6 +7,7 @@ android.namespace = "com.flipperdevices.filemanager.editor.api" commonDependencies { implementation(projects.components.core.ui.decompose) + implementation(projects.components.bridge.connection.feature.storage.api) implementation(libs.compose.ui) implementation(libs.decompose) diff --git a/components/filemngr/editor/api/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponent.kt b/components/filemngr/editor/api/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponent.kt index 13f1c89318..838f4d1267 100644 --- a/components/filemngr/editor/api/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponent.kt +++ b/components/filemngr/editor/api/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponent.kt @@ -1,18 +1,18 @@ package com.flipperdevices.filemanager.editor.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.ui.decompose.CompositeDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter -import com.flipperdevices.ui.decompose.ScreenDecomposeComponent import okio.Path -abstract class FileManagerEditorDecomposeComponent( - componentContext: ComponentContext -) : ScreenDecomposeComponent(componentContext) { +abstract class FileManagerEditorDecomposeComponent : CompositeDecomposeComponent() { fun interface Factory { operator fun invoke( componentContext: ComponentContext, onBack: DecomposeOnBackParameter, + onFileChanged: (ListingItem) -> Unit, path: Path - ): FileManagerEditorDecomposeComponent + ): FileManagerEditorDecomposeComponent<*> } } diff --git a/components/filemngr/editor/impl/build.gradle.kts b/components/filemngr/editor/impl/build.gradle.kts index f6c8fe2d81..7e2b622bec 100644 --- a/components/filemngr/editor/impl/build.gradle.kts +++ b/components/filemngr/editor/impl/build.gradle.kts @@ -34,7 +34,6 @@ commonDependencies { implementation(projects.components.filemngr.uiComponents) implementation(projects.components.filemngr.editor.api) - implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.main.api) implementation(projects.components.filemngr.util) diff --git a/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml index 11eccc0ad9..5aee40d618 100644 --- a/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml +++ b/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml @@ -3,6 +3,10 @@ The file is larger than 1MB and therefore only part of the file is shown. Save File as... Save + Downloading... + Uploading: %1$s [%2$s/%3$s] + Speed: %1$s/s + Cancel Allowed characters: %1$s TXT HEX diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditFileNameDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditFileNameDecomposeComponent.kt new file mode 100644 index 0000000000..5bb33b9728 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditFileNameDecomposeComponent.kt @@ -0,0 +1,46 @@ +package com.flipperdevices.filemanager.editor.api + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.filemanager.editor.composable.dialog.CreateFileDialogComposable +import com.flipperdevices.filemanager.editor.viewmodel.EditFileNameViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import okio.Path + +class EditFileNameDecomposeComponent @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted("fullPathOnFlipper") private val fullPathOnFlipper: Path, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted private val onChanged: (Path) -> Unit, + editFileNameViewModelFactory: EditFileNameViewModel.Factory +) : ScreenDecomposeComponent(componentContext) { + private val editFileNameViewModel = instanceKeeper.getOrCreate { + editFileNameViewModelFactory.invoke(fullPathOnFlipper) + } + + @Composable + override fun Render() { + CreateFileDialogComposable( + editFileNameViewModel = editFileNameViewModel, + onFinish = { name -> + fullPathOnFlipper.parent?.resolve(name)?.run(onChanged) + }, + onDismiss = onBack::invoke + ) + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + onBack: DecomposeOnBackParameter, + onChanged: (Path) -> Unit + ): EditFileNameDecomposeComponent + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditorDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditorDecomposeComponent.kt new file mode 100644 index 0000000000..ccbfa9969e --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/EditorDecomposeComponent.kt @@ -0,0 +1,102 @@ +package com.flipperdevices.filemanager.editor.api + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.filemanager.editor.composable.FileManagerEditorComposable +import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.serialization.Serializable +import okio.Path + +@Suppress("LongParameterList") +class EditorDecomposeComponent @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + @Assisted private val editFinishedCallback: EditFinishedCallback, + editorViewModelFactory: EditorViewModel.Factory, + editFileNameDecomposeComponentFactory: EditFileNameDecomposeComponent.Factory +) : ScreenDecomposeComponent(componentContext) { + + private val editorViewModel = instanceKeeper.getOrCreate { + editorViewModelFactory.invoke( + fullPathOnFlipper = fullPathOnFlipper, + fullPathOnDevice = fullPathOnDevice + ) + } + + private fun saveFile() { + editorViewModel.writeNow() + editFinishedCallback.invoke( + fullPathOnFlipper = editorViewModel.state.value.fullPathOnFlipper + ) + } + + @Serializable + sealed interface SlotConfiguration { + data object ChangeFlipperFileName : SlotConfiguration + } + + private val slotNavigation = SlotNavigation() + + private val fileOptionsSlot = childSlot( + source = slotNavigation, + handleBackButton = true, + serializer = SlotConfiguration.serializer(), + childFactory = { config, childContext -> + when (config) { + SlotConfiguration.ChangeFlipperFileName -> { + editFileNameDecomposeComponentFactory.invoke( + componentContext = childContext, + fullPathOnFlipper = editorViewModel.state.value.fullPathOnFlipper, + onBack = slotNavigation::dismiss, + onChanged = { fullPathOnFlipper -> + editorViewModel.onFlipperPathChanged(fullPathOnFlipper) + slotNavigation.dismiss() + saveFile() + } + ) + } + } + } + ) + + @Composable + override fun Render() { + FileManagerEditorComposable( + editorViewModel = editorViewModel, + onBack = onBack::invoke, + onSaveAsClick = { + slotNavigation.activate(SlotConfiguration.ChangeFlipperFileName) + }, + onSaveClick = { saveFile() } + ) + fileOptionsSlot.subscribeAsState().value.child?.instance?.Render() + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + onBack: DecomposeOnBackParameter, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + editFinishedCallback: EditFinishedCallback + ): EditorDecomposeComponent + } + + fun interface EditFinishedCallback { + fun invoke(fullPathOnFlipper: Path) + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt new file mode 100644 index 0000000000..ccc900748e --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt @@ -0,0 +1,97 @@ +package com.flipperdevices.filemanager.editor.api + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.filemanager.editor.composable.download.UploadingComposable +import com.flipperdevices.filemanager.editor.viewmodel.DownloadViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_downloading +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import okio.Path +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME + +class FileDownloadDecomposeComponent @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + @Assisted private val onDownloaded: () -> Unit, + @Assisted private val onBack: DecomposeOnBackParameter, + downloadViewModelFactory: DownloadViewModel.Factory +) : ScreenDecomposeComponent(componentContext) { + private val downloadViewModel = instanceKeeper.getOrCreate { + downloadViewModelFactory.invoke( + fullPathOnFlipper = fullPathOnFlipper, + fullPathOnDevice = fullPathOnDevice + ) + } + + @Composable + override fun Render() { + LaunchedEffect(downloadViewModel) { + downloadViewModel.state + .filterIsInstance() + .onEach { + onDownloaded.invoke() + }.launchIn(this) + } + val state by downloadViewModel.state.collectAsState() + + when (val localState = state) { + DownloadViewModel.State.CouldNotDownload -> { + Box(Modifier.fillMaxSize().background(Color.Red)) + } + + DownloadViewModel.State.Downloaded -> { + Box(Modifier.fillMaxSize().background(Color.Green)) + } + + is DownloadViewModel.State.Downloading -> { + UploadingComposable( + progress = localState.progress, + fullPathOnFlipper = localState.fullPathOnFlipper, + current = localState.downloaded, + max = localState.total, + speed = downloadViewModel.speedState.collectAsState().value, + onCancel = onBack::invoke, + modifier = Modifier, + title = stringResource(FME.string.fme_status_downloading) + ) + } + + DownloadViewModel.State.TooLarge -> { + Box(Modifier.fillMaxSize().background(Color.Cyan)) + } + + DownloadViewModel.State.Unsupported -> { + Box(Modifier.fillMaxSize().background(Color.Yellow)) + } + } + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + onBack: DecomposeOnBackParameter, + onDownloaded: () -> Unit + ): FileDownloadDecomposeComponent + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponentImpl.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponentImpl.kt index bcae8f6d74..79551db10b 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponentImpl.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileManagerEditorDecomposeComponentImpl.kt @@ -1,66 +1,102 @@ package com.flipperdevices.filemanager.editor.api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.childContext +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.replaceCurrent +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.core.FlipperStorageProvider import com.flipperdevices.core.di.AppGraph -import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory -import com.flipperdevices.filemanager.editor.composable.FileManagerEditorComposable -import com.flipperdevices.filemanager.editor.composable.content.RenderLoadingScreen -import com.flipperdevices.filemanager.editor.composable.dialog.CreateFileDialogComposable -import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel -import com.flipperdevices.filemanager.editor.viewmodel.FileNameViewModel -import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent +import com.flipperdevices.filemanager.editor.model.FileManagerEditorConfiguration +import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.popOr import dagger.assisted.Assisted import dagger.assisted.AssistedInject import me.gulya.anvil.assisted.ContributesAssistedFactory import okio.Path -import javax.inject.Provider +@Suppress("LongParameterList") @ContributesAssistedFactory(AppGraph::class, FileManagerEditorDecomposeComponent.Factory::class) class FileManagerEditorDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted private val path: Path, @Assisted private val onBack: DecomposeOnBackParameter, - private val editorViewModelFactory: EditorViewModel.Factory, - uploaderDecomposeComponentFactory: UploaderDecomposeComponent.Factory, - private val fileNameViewModelProvider: Provider, -) : FileManagerEditorDecomposeComponent(componentContext) { - private val uploaderDecomposeComponent = uploaderDecomposeComponentFactory.invoke( - componentContext = childContext("file_editor_$path") - ) - - @Composable - override fun Render() { - val fileNameViewModel = viewModelWithFactory(null) { - fileNameViewModelProvider.get() - } - val editorViewModel = viewModelWithFactory(path.toString()) { - editorViewModelFactory.invoke(path) + @Assisted private val onFileChanged: (ListingItem) -> Unit, + fileDownloadDecomposeComponentFactory: FileDownloadDecomposeComponent.Factory, + private val editorDecomposeComponentFactory: EditorDecomposeComponent.Factory, + private val storageProvider: FlipperStorageProvider, + private val uploadFileDecomposeComponentFactory: UploadFileDecomposeComponent.Factory +) : FileManagerEditorDecomposeComponent(), + ComponentContext by componentContext { + private val editorFileKeeper = instanceKeeper.getOrCreate { + object : InstanceKeeper.Instance { + val editorFile = storageProvider.getTemporaryFile() + override fun onDestroy() { + storageProvider.fileSystem.delete(editorFile) + } } + } - CreateFileDialogComposable( - fileNameViewModel = fileNameViewModel, - onFinish = onSaveClick@{ fileName -> - val rawContent = editorViewModel.getRawContent() ?: return@onSaveClick - uploaderDecomposeComponent.uploadRaw( - folderPath = path.parent ?: return@onSaveClick, - fileName = fileName, - content = rawContent - ) - } - ) + override val stack: Value> = + childStack( + source = navigation, + serializer = FileManagerEditorConfiguration.serializer(), + initialStack = { listOf(FileManagerEditorConfiguration.Download(path)) }, + handleBackButton = true, + childFactory = { config, childContext -> + when (config) { + is FileManagerEditorConfiguration.Download -> { + fileDownloadDecomposeComponentFactory.invoke( + componentContext = childContext, + fullPathOnFlipper = path, + fullPathOnDevice = editorFileKeeper.editorFile, + onBack = { navigation.popOr(onBack::invoke) }, + onDownloaded = { + navigation.replaceCurrent( + FileManagerEditorConfiguration.Editor( + fullPathOnFlipper = config.fullPathOnFlipper, + tempPathOnDevice = editorFileKeeper.editorFile + ) + ) + } + ) + } - FileManagerEditorComposable( - path = path, - editorViewModel = editorViewModel, - uploaderDecomposeComponent = uploaderDecomposeComponent, - fileNameViewModel = fileNameViewModel, - onBack = onBack::invoke - ) + is FileManagerEditorConfiguration.Editor -> { + editorDecomposeComponentFactory.invoke( + componentContext = componentContext, + fullPathOnFlipper = config.fullPathOnFlipper, + fullPathOnDevice = config.tempPathOnDevice, + onBack = { navigation.popOr(onBack::invoke) }, + editFinishedCallback = { fullPathOnFlipper -> + navigation.pushNew( + FileManagerEditorConfiguration.Upload( + fullPathOnFlipper = fullPathOnFlipper, + tempPathOnDevice = editorFileKeeper.editorFile + ) + ) + } + ) + } - uploaderDecomposeComponent.RenderLoadingScreen() - } + is FileManagerEditorConfiguration.Upload -> { + uploadFileDecomposeComponentFactory.invoke( + componentContext = childContext, + onBack = { navigation.popOr(onBack::invoke) }, + fullPathOnFlipper = config.fullPathOnFlipper, + fullPathOnDevice = config.tempPathOnDevice, + onFinished = { + navigation.popOr(onBack::invoke) + }, + onProgress = onFileChanged + ) + } + } + }, + ) } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt new file mode 100644 index 0000000000..a5ab5c0e3d --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt @@ -0,0 +1,109 @@ +package com.flipperdevices.filemanager.editor.api + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem +import com.flipperdevices.filemanager.editor.composable.download.UploadingComposable +import com.flipperdevices.filemanager.editor.viewmodel.UploadFileViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_uploading +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import okio.Path +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME + +@Suppress("LongParameterList") +class UploadFileDecomposeComponent @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + @Assisted private val onProgress: (ListingItem) -> Unit, + @Assisted private val onFinished: () -> Unit, + uploadFileViewModelFactory: UploadFileViewModel.Factory +) : ScreenDecomposeComponent(componentContext) { + private val uploadFileViewModel = instanceKeeper.getOrCreate { + uploadFileViewModelFactory.invoke( + fullPathOnFlipper = fullPathOnFlipper, + fullPathOnDevice = fullPathOnDevice, + ) + } + + @Composable + override fun Render() { + LaunchedEffect(uploadFileViewModel) { + uploadFileViewModel.state + .filterIsInstance() + .onEach { state -> + val listingItem = ListingItem( + fileName = state.fullPathOnFlipper.name, + fileType = FileType.FILE, + size = state.uploaded + ) + onProgress.invoke(listingItem) + }.launchIn(this) + + uploadFileViewModel.state + .filterIsInstance() + .onEach { onFinished.invoke() } + .launchIn(this) + } + + val state by uploadFileViewModel.state.collectAsState() + + when (val localState = state) { + UploadFileViewModel.State.Error -> { + Box(Modifier.fillMaxSize().background(Color.Red)) + } + + is UploadFileViewModel.State.Saved -> { + Box(Modifier.fillMaxSize().background(Color.Green)) + } + + UploadFileViewModel.State.Unsupported -> { + Box(Modifier.fillMaxSize().background(Color.Yellow)) + } + + is UploadFileViewModel.State.Uploading -> { + UploadingComposable( + progress = localState.progress, + fullPathOnFlipper = localState.fullPathOnFlipper, + current = localState.uploaded, + max = localState.total, + speed = uploadFileViewModel.speedState.collectAsState().value, + onCancel = onBack::invoke, + modifier = Modifier, + title = stringResource(FME.string.fme_status_uploading) + ) + } + } + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + onBack: DecomposeOnBackParameter, + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + onProgress: (ListingItem) -> Unit, + onFinished: () -> Unit, + ): UploadFileDecomposeComponent + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/EditorDropdown.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/EditorDropdown.kt index c3f413587f..34e38ec141 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/EditorDropdown.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/EditorDropdown.kt @@ -50,11 +50,17 @@ internal fun EditorDropdown( ) { TextDropdownItem( text = stringResource(FME.string.fme_save), - onClick = onSaveClick + onClick = { + onSaveClick.invoke() + isDropdownVisible = !isDropdownVisible + } ) TextDropdownItem( text = stringResource(FME.string.fme_save_as_file), - onClick = onSaveAsClick + onClick = { + onSaveAsClick.invoke() + isDropdownVisible = !isDropdownVisible + } ) } } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/FileManagerEditorComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/FileManagerEditorComposable.kt index cad93b8a24..e7400be79b 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/FileManagerEditorComposable.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/FileManagerEditorComposable.kt @@ -6,21 +6,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.flipperdevices.filemanager.editor.composable.content.EditorLoadingContent -import com.flipperdevices.filemanager.editor.composable.content.ErrorContent import com.flipperdevices.filemanager.editor.composable.content.LoadedContent import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel -import com.flipperdevices.filemanager.editor.viewmodel.FileNameViewModel -import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent -import okio.Path @Composable fun FileManagerEditorComposable( - path: Path, editorViewModel: EditorViewModel, - uploaderDecomposeComponent: UploaderDecomposeComponent, - fileNameViewModel: FileNameViewModel, onBack: () -> Unit, + onSaveAsClick: () -> Unit, + onSaveClick: () -> Unit, modifier: Modifier = Modifier ) { val editorState by editorViewModel.state.collectAsState() @@ -29,48 +23,20 @@ fun FileManagerEditorComposable( modifier = modifier, topBar = { EditorAppBar( - path = path, - onSaveClick = onSaveClick@{ - val rawContent = editorViewModel.getRawContent() ?: return@onSaveClick - uploaderDecomposeComponent.uploadRaw( - folderPath = path.parent ?: return@onSaveClick, - fileName = path.name, - content = rawContent - ) - }, - onSaveAsClick = { - fileNameViewModel.show() - }, + path = editorState.fullPathOnFlipper, + onSaveClick = onSaveClick::invoke, + onSaveAsClick = onSaveAsClick, onBack = onBack::invoke, - editorEncodingEnum = (editorState as? EditorViewModel.State.Loaded)?.encoding, - canSave = (editorState is EditorViewModel.State.Loaded), + editorEncodingEnum = editorState.encoding, + canSave = editorState.canEdit, onEditorTabChange = editorViewModel::onEditorTypeChange ) } ) { contentPadding -> - when (val localEditorState = editorState) { - EditorViewModel.State.Error -> { - ErrorContent( - modifier = Modifier.padding(contentPadding) - ) - } - - EditorViewModel.State.Preparing, - is EditorViewModel.State.Loading -> { - EditorLoadingContent( - modifier = Modifier.padding(contentPadding) - ) - } - - is EditorViewModel.State.Loaded -> { - LoadedContent( - state = localEditorState, - onTextChange = editorViewModel::onTextChanged, - modifier = Modifier.padding(contentPadding) - ) - } - - is EditorViewModel.State.Saving -> Unit - } + LoadedContent( + state = editorState, + onTextChange = editorViewModel::onTextChanged, + modifier = Modifier.padding(contentPadding) + ) } } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/LoadedContent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/LoadedContent.kt index 1c90c50ac9..dbae9f9c69 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/LoadedContent.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/LoadedContent.kt @@ -11,19 +11,16 @@ import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel @Composable fun LoadedContent( - state: EditorViewModel.State.Loaded, + state: EditorViewModel.State, onTextChange: (String) -> Unit, modifier: Modifier = Modifier ) { Column(modifier = modifier) { - if (state.isTooLarge) { - TooBigContent() - } TextField( modifier = Modifier.fillMaxSize(), value = state.hexString.content, - enabled = !state.isTooLarge, - readOnly = state.isTooLarge, + enabled = state.canEdit, + readOnly = !state.canEdit, onValueChange = onTextChange, colors = TextFieldDefaults.textFieldColors( cursorColor = LocalPallet.current.text100 diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/UploadingContent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/UploadingContent.kt deleted file mode 100644 index 67b7067d1c..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/UploadingContent.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable.content - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent - -@Composable -fun UploaderDecomposeComponent.RenderLoadingScreen(modifier: Modifier = Modifier) { - val uploaderState by state.collectAsState() - AnimatedVisibility( - visible = uploaderState is UploaderDecomposeComponent.State.Uploading, - enter = fadeIn(), - exit = fadeOut(), - modifier = modifier.fillMaxSize() - ) { - val speedState by speedState.collectAsState(null) - Render( - state = uploaderState, - speedState = speedState, - onCancelClick = ::onCancel, - modifier = Modifier - .fillMaxSize() - .background(LocalPalletV2.current.surface.backgroundMain.body) - .navigationBarsPadding() - .systemBarsPadding(), - ) - } -} 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 9fd7c2e2ef..b240bfd164 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 @@ -3,7 +3,7 @@ package com.flipperdevices.filemanager.editor.composable.dialog import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import com.flipperdevices.filemanager.editor.viewmodel.FileNameViewModel +import com.flipperdevices.filemanager.editor.viewmodel.EditFileNameViewModel 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 @@ -14,34 +14,28 @@ import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME @Composable fun CreateFileDialogComposable( - fileNameViewModel: FileNameViewModel, - onFinish: (String) -> Unit + editFileNameViewModel: EditFileNameViewModel, + onFinish: (String) -> Unit, + onDismiss: () -> Unit ) { - val state by fileNameViewModel.state.collectAsState() - when (val localState = state) { - is FileNameViewModel.State.Editing -> { - NameDialog( - value = localState.name, - title = stringResource(FME.string.fme_save_as_dialog_title), - buttonText = stringResource(FME.string.fme_save_as_dialog_button), - subtitle = stringResource( - resource = FME.string.fme_save_as_dialog_chars, - FileManagerConstants.FILE_NAME_AVAILABLE_CHARACTERS - ), - onFinish = { - onFinish(localState.name) - fileNameViewModel.dismiss() - }, - isError = !localState.isValid, - isEnabled = true, - needShowOptions = localState.needShowOptions, - onTextChange = fileNameViewModel::onChange, - onDismissRequest = fileNameViewModel::dismiss, - onOptionSelect = fileNameViewModel::onOptionSelected, - options = localState.options - ) - } - - FileNameViewModel.State.Pending -> Unit - } + val state by editFileNameViewModel.state.collectAsState() + NameDialog( + value = state.name, + title = stringResource(FME.string.fme_save_as_dialog_title), + buttonText = stringResource(FME.string.fme_save_as_dialog_button), + subtitle = stringResource( + resource = FME.string.fme_save_as_dialog_chars, + FileManagerConstants.FILE_NAME_AVAILABLE_CHARACTERS + ), + onFinish = { + onFinish(state.name) + }, + isError = !state.isValid, + isEnabled = true, + needShowOptions = state.needShowOptions, + onTextChange = editFileNameViewModel::onChange, + onDismissRequest = onDismiss, + onOptionSelect = editFileNameViewModel::onOptionSelected, + options = state.options + ) } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt new file mode 100644 index 0000000000..a81c80ab58 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt @@ -0,0 +1,93 @@ +package com.flipperdevices.filemanager.editor.composable.download + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ktx.jre.toFormattedSize +import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_speed +import okio.Path +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME + +@Composable +private fun InProgressTitleComposable( + fullPathOnFlipper: Path, + modifier: Modifier = Modifier +) { + Text( + text = fullPathOnFlipper.name, + style = LocalTypography.current.titleB18, + color = LocalPalletV2.current.text.title.primary, + modifier = modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) +} + +@Composable +internal fun InProgressComposable( + progress: Float, + fullPathOnFlipper: Path, + downloaded: Long, + total: Long, + speed: Long, +) { + val animatedProgress by animateFloatAsState( + targetValue = progress, + animationSpec = tween(durationMillis = 500, easing = LinearEasing), + label = "Progress" + ) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + InProgressTitleComposable(fullPathOnFlipper) + Spacer(Modifier.height(12.dp)) + FlipperProgressIndicator( + modifier = Modifier.padding(horizontal = 32.dp), + accentColor = LocalPalletV2.current.action.blue.border.primary.default, + secondColor = LocalPallet.current.actionOnFlipperProgress, + painter = null, + percent = animatedProgress + ) + Spacer(Modifier.height(8.dp)) + if (total > 0) { + Text( + text = "${downloaded.toFormattedSize()}/${total.toFormattedSize()}", + style = LocalTypography.current.subtitleM12, + color = LocalPalletV2.current.text.body.secondary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + Spacer(Modifier.height(8.dp)) + if (speed > 0) { + Text( + text = stringResource( + resource = FME.string.fme_status_speed, + speed.toFormattedSize() + ), + style = LocalTypography.current.subtitleM12, + color = LocalPalletV2.current.text.body.secondary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt new file mode 100644 index 0000000000..4f297d5c67 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt @@ -0,0 +1,74 @@ +package com.flipperdevices.filemanager.editor.composable.download + +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.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_cancel +import okio.Path +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME + +@Composable +fun UploadingComposable( + title: String, + progress: Float, + fullPathOnFlipper: Path, + current: Long, + max: Long, + speed: Long, + onCancel: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = title, + style = LocalTypography.current.titleB18, + color = LocalPalletV2.current.text.title.primary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + + Text( + text = stringResource(FME.string.fme_cancel), + style = LocalTypography.current.bodyM14, + color = LocalPalletV2.current.action.danger.text.default, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .clip(RoundedCornerShape(12.dp)) + .clickableRipple(onClick = onCancel), + textAlign = TextAlign.Center + ) + } + InProgressComposable( + progress = progress, + speed = speed, + fullPathOnFlipper = fullPathOnFlipper, + downloaded = current, + total = max + ) + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/model/FileManagerEditorConfiguration.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/model/FileManagerEditorConfiguration.kt new file mode 100644 index 0000000000..5b5db4bb75 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/model/FileManagerEditorConfiguration.kt @@ -0,0 +1,30 @@ +package com.flipperdevices.filemanager.editor.model + +import com.flipperdevices.filemanager.util.serialization.PathSerializer +import kotlinx.serialization.Serializable +import okio.Path + +@Serializable +sealed interface FileManagerEditorConfiguration { + @Serializable + data class Download( + @Serializable(PathSerializer::class) + val fullPathOnFlipper: Path + ) : FileManagerEditorConfiguration + + @Serializable + data class Editor( + @Serializable(PathSerializer::class) + val fullPathOnFlipper: Path, + @Serializable(PathSerializer::class) + val tempPathOnDevice: Path + ) : FileManagerEditorConfiguration + + @Serializable + data class Upload( + @Serializable(PathSerializer::class) + val fullPathOnFlipper: Path, + @Serializable(PathSerializer::class) + val tempPathOnDevice: Path + ) : FileManagerEditorConfiguration +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/DownloadViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/DownloadViewModel.kt new file mode 100644 index 0000000000..95ed082aad --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/DownloadViewModel.kt @@ -0,0 +1,134 @@ +package com.flipperdevices.filemanager.editor.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.serialspeed.api.FSpeedFeatureApi +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.core.FlipperStorageProvider +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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import okio.Path + +class DownloadViewModel @AssistedInject constructor( + @Assisted("fullPathOnFlipper") private val fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") private val fullPathOnDevice: Path, + private val featureProvider: FFeatureProvider, + private val storageProvider: FlipperStorageProvider +) : DecomposeViewModel(), LogTagProvider { + override val TAG = "DownloadViewModel" + private val _state = MutableStateFlow( + State.Downloading( + downloaded = 0, + total = 0, + fullPathOnFlipper = fullPathOnFlipper + ) + ) + val state = _state.asStateFlow() + + val speedState = featureProvider.get() + .stateIn(viewModelScope, SharingStarted.Eagerly, FFeatureStatus.Retrieving) + .flatMapLatest { + (it as? FFeatureStatus.Supported) + ?.featureApi + ?.getSpeed() + ?.map { speedState -> speedState.receiveBytesInSec } + ?: flowOf(0L) + }.stateIn(viewModelScope, SharingStarted.Eagerly, 0L) + + private fun startFeature() { + featureProvider.get() + .onEach { status -> + when (status) { + FFeatureStatus.NotFound -> _state.emit(State.Unsupported) + FFeatureStatus.Retrieving -> _state.emit( + State.Downloading( + downloaded = 0, + total = 0, + fullPathOnFlipper = fullPathOnFlipper + ) + ) + + is FFeatureStatus.Supported -> { + status.featureApi.downloadApi().download( + pathOnFlipper = fullPathOnFlipper.toString(), + fileOnAndroid = fullPathOnDevice, + progressListener = { current, max -> + _state.emit( + State.Downloading( + downloaded = current, + total = max, + fullPathOnFlipper = fullPathOnFlipper + ) + ) + } + ).onFailure { throwable -> + error(throwable) { "Failed to download file $fullPathOnFlipper" } + _state.emit(State.CouldNotDownload) + }.onSuccess { + val metaSize = + storageProvider.fileSystem.metadataOrNull(fullPathOnDevice)?.size + val isTooLarge = if (metaSize != null) { + metaSize > FileManagerConstants.LIMITED_SIZE_BYTES + } else { + false + } + if (isTooLarge) { + _state.emit(State.TooLarge) + } else { + _state.emit(State.Downloaded) + } + } + } + + FFeatureStatus.Unsupported -> _state.emit(State.Unsupported) + } + } + .launchIn(viewModelScope) + } + + init { + startFeature() + } + + sealed interface State { + data object Unsupported : State + data object CouldNotDownload : State + data class Downloading( + val downloaded: Long, + val total: Long, + val fullPathOnFlipper: Path + ) : State { + val progress: Float = when (total) { + 0L -> 0f + else -> (downloaded / total.toFloat()).coerceIn(0f, 1f) + } + } + + data object TooLarge : State + + data object Downloaded : State + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + ): DownloadViewModel + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt new file mode 100644 index 0000000000..32517b1f76 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt @@ -0,0 +1,65 @@ +package com.flipperdevices.filemanager.editor.viewmodel + +import com.flipperdevices.core.ktx.jre.FlipperFileNameValidator +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.filemanager.util.constant.FileManagerConstants +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import okio.Path + +class EditFileNameViewModel @AssistedInject constructor( + @Assisted fullPathOnFlipper: Path +) : DecomposeViewModel() { + private val fileNameValidator = FlipperFileNameValidator() + + private val _state = MutableStateFlow(State(fullPathOnFlipper.name)) + val state = _state.asStateFlow() + + fun onChange(text: String) { + _state.update { state -> + state.copy(name = text) + } + } + + fun onOptionSelected(index: Int) { + _state.update { state -> + val option = state.options + .getOrNull(index) + ?: return@update state + state.copy(name = "${state.name}.$option") + } + } + + init { + state + .distinctUntilChangedBy { state -> state.name } + .onEach { state -> + _state.emit(state.copy(isValid = fileNameValidator.isValid(state.name))) + }.launchIn(viewModelScope) + } + + data class State( + val name: String = "", + val isValid: Boolean = false + ) { + val options = FileManagerConstants.FILE_EXTENSION_HINTS.toImmutableList() + + val needShowOptions + get() = !name.contains(".") && options.isNotEmpty() + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + fullPathOnFlipper: Path, + ): EditFileNameViewModel + } +} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditorViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditorViewModel.kt index d286f7c274..04627241eb 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditorViewModel.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditorViewModel.kt @@ -1,46 +1,45 @@ package com.flipperdevices.filemanager.editor.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.core.FlipperStorageProvider import com.flipperdevices.core.ktx.jre.limit -import com.flipperdevices.core.ktx.jre.withLock -import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import com.flipperdevices.filemanager.editor.model.EditorEncodingEnum import com.flipperdevices.filemanager.editor.model.HexString import com.flipperdevices.filemanager.editor.util.HexConverter +import com.flipperdevices.filemanager.util.constant.FileManagerConstants import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.launch +import okio.Buffer import okio.Path import okio.buffer import okio.use class EditorViewModel @AssistedInject constructor( - @Assisted private val path: Path, - private val featureProvider: FFeatureProvider, - private val storageProvider: com.flipperdevices.core.FlipperStorageProvider -) : DecomposeViewModel(), LogTagProvider { - override val TAG = "EditorViewModel" - - private val _state = MutableStateFlow(State.Preparing) + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") private val fullPathOnDevice: Path, + private val storageProvider: FlipperStorageProvider +) : DecomposeViewModel() { + + private val _state = MutableStateFlow( + State( + fullPathOnFlipper = fullPathOnFlipper, + hexString = HexString.Text("") + ) + ) val state = _state.asStateFlow() - private val mutex = Mutex() - - private val editorFile by lazy { storageProvider.getTemporaryFile() } + fun onFlipperPathChanged(fullPathOnFlipper: Path) { + _state.update { state -> state.copy(fullPathOnFlipper = fullPathOnFlipper) } + } fun getRawContent(): ByteArray? { - val hexString = (state.value as? EditorViewModel.State.Loaded)?.hexString ?: return null + val hexString = state.value.hexString return kotlin.runCatching { HexConverter.fromHexString(hexString).content.toByteArray() } .onFailure { error(it) { "#onEditorTypeChange could not transform hex" } } .getOrNull() @@ -48,138 +47,78 @@ class EditorViewModel @AssistedInject constructor( fun onTextChanged(text: String) { _state.update { state -> - (state as? State.Loaded)?.let { loadedState -> - when (loadedState.hexString) { - is HexString.Hex -> loadedState.copy(hexString = HexString.Hex(text)) - is HexString.Text -> loadedState.copy(hexString = HexString.Text(text)) - } - } ?: state - } - } - - private suspend fun onFileDownloaded() { - val content = storageProvider - .fileSystem - .source(editorFile) - .limit(LIMITED_SIZE_BYTES) - .buffer() - .use { bufferedSource -> bufferedSource.readUtf8() } - val metaSize = storageProvider.fileSystem.metadataOrNull(editorFile)?.size - val isTooLarge = if (metaSize != null) { - metaSize > LIMITED_SIZE_BYTES - } else { - false - } - _state.emit( - State.Loaded( - path = path, - hexString = HexString.Text(content), - isTooLarge = isTooLarge - ) - ) - } - - private suspend fun loadFileSafe(storageFeatureApi: FStorageFeatureApi) { - _state.emit(State.Preparing) - storageFeatureApi.downloadApi().download( - pathOnFlipper = path.toString(), - fileOnAndroid = editorFile, - progressListener = { current, max -> - _state.emit( - State.Loading( - downloaded = current, - total = max - ) - ) + when (state.hexString) { + is HexString.Hex -> state.copy(hexString = HexString.Hex(text)) + is HexString.Text -> state.copy(hexString = HexString.Text(text)) } - ).onFailure { exception -> - error(exception) { "Failed download $path" } - _state.emit(State.Error) - }.onSuccess { - onFileDownloaded() - } - } - - private suspend fun onFeatureStateChanged( - featureStatus: FFeatureStatus - ) = withLock(mutex, "feature") { - when (featureStatus) { - FFeatureStatus.NotFound, - FFeatureStatus.Unsupported -> _state.emit(State.Error) - - FFeatureStatus.Retrieving -> _state.emit(State.Preparing) - - is FFeatureStatus.Supported -> loadFileSafe(featureStatus.featureApi) } } - init { - featureProvider.get() - .onEach { onFeatureStateChanged(it) } - .launchIn(viewModelScope) - } - - override fun onDestroy() { - super.onDestroy() - storageProvider.fileSystem.delete(editorFile) - } - fun onEditorTypeChange(type: EditorEncodingEnum) { + _state.update { state -> state.copy(canEdit = false) } _state.update { state -> - val loaded = (state as? State.Loaded) ?: return@update state - if (loaded.encoding == type) return@update state + if (state.encoding == type) return@update state // We can't always transform hex into string // As example, user can write letters beyond 0..F val hexStringResult = kotlin.runCatching { when (type) { EditorEncodingEnum.TEXT -> { - HexConverter.fromHexString(loaded.hexString) + HexConverter.fromHexString(state.hexString) } EditorEncodingEnum.HEX -> { - HexConverter.toHexString(loaded.hexString) + HexConverter.toHexString(state.hexString) } } }.onFailure { error(it) { "#onEditorTypeChange could not transform hex" } } - loaded.copy( - encoding = type - .takeIf { hexStringResult.isSuccess } - ?: loaded.encoding, - hexString = hexStringResult.getOrElse { loaded.hexString } + state.copy( + hexString = hexStringResult.getOrElse { state.hexString }, + canEdit = true ) } } - @AssistedFactory - fun interface Factory { - operator fun invoke(path: Path): EditorViewModel + fun writeNow() { + val byteArray = getRawContent() ?: return + val buffer = Buffer() + buffer.write(byteArray) + storageProvider + .fileSystem + .sink(fullPathOnDevice) + .write(buffer, byteArray.size.toLong()) } - sealed interface State { - data object Preparing : State - - data class Loading( - val downloaded: Long, - val total: Long - ) : State - - data class Saving( - val uploaded: Long, - val total: Long - ) : State + private fun loadText() { + val content = storageProvider + .fileSystem + .source(fullPathOnDevice) + .limit(FileManagerConstants.LIMITED_SIZE_BYTES) + .buffer() + .use { bufferedSource -> bufferedSource.readUtf8() } + _state.update { state -> state.copy(hexString = HexString.Text(content)) } + } - data class Loaded( - val path: Path, - val hexString: HexString, - val isTooLarge: Boolean, - val encoding: EditorEncodingEnum = EditorEncodingEnum.TEXT - ) : State + init { + viewModelScope.launch { loadText() } + } - data object Error : State + data class State( + val fullPathOnFlipper: Path, + val hexString: HexString, + val canEdit: Boolean = true + ) { + val encoding: EditorEncodingEnum = when (hexString) { + is HexString.Hex -> EditorEncodingEnum.HEX + is HexString.Text -> EditorEncodingEnum.TEXT + } } - companion object { - private const val LIMITED_SIZE_BYTES = 1024L * 1024L // 1MB + @AssistedFactory + fun interface Factory { + operator fun invoke( + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + ): EditorViewModel } } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/FileNameViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/FileNameViewModel.kt deleted file mode 100644 index 9e0ae9fb98..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/FileNameViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.flipperdevices.filemanager.editor.viewmodel - -import com.flipperdevices.bridge.dao.api.model.FlipperKeyType -import com.flipperdevices.core.ktx.jre.FlipperFileNameValidator -import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -class FileNameViewModel @Inject constructor() : DecomposeViewModel() { - - private val fileNameValidator = FlipperFileNameValidator() - - private val _state = MutableStateFlow(State.Pending) - val state = _state.asStateFlow() - - fun onChange(text: String) { - _state.update { state -> - (state as? State.Editing) - ?.copy(name = text) - ?: state - } - } - - fun dismiss() { - _state.update { State.Pending } - } - - fun onOptionSelected(index: Int) { - _state.update { state -> - (state as? State.Editing)?.let { editingState -> - val option = editingState.options - .getOrNull(index) - ?: return@let editingState - editingState.copy(name = option) - } ?: state - } - } - - fun show() { - _state.update { State.Editing() } - } - - init { - state - .filterIsInstance() - .distinctUntilChangedBy { state -> state.name } - .onEach { state -> - _state.emit(state.copy(isValid = fileNameValidator.isValid(state.name))) - }.launchIn(viewModelScope) - } - - sealed interface State { - data object Pending : State - data class Editing( - val name: String = "", - val isValid: Boolean = false - ) : State { - val options = listOf("txt") - .plus(FlipperKeyType.entries.map { it.extension }) - .map { extension -> "$name.$extension" } - .toImmutableList() - - val needShowOptions - get() = !name.contains(".") && options.isNotEmpty() - } - } -} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/UploadFileViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/UploadFileViewModel.kt new file mode 100644 index 0000000000..4ec229ce84 --- /dev/null +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/UploadFileViewModel.kt @@ -0,0 +1,132 @@ +package com.flipperdevices.filemanager.editor.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.serialspeed.api.FSpeedFeatureApi +import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import okio.Path + +class UploadFileViewModel @AssistedInject constructor( + @Assisted("fullPathOnFlipper") private val fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") private val fullPathOnDevice: Path, + private val featureProvider: FFeatureProvider, +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "UploadViewModel" + + private val _state = MutableStateFlow( + State.Uploading( + fullPathOnFlipper = fullPathOnFlipper, + uploaded = 0L, + total = 0L + ) + ) + val state = _state.asStateFlow() + + val speedState = featureProvider.get() + .stateIn(viewModelScope, SharingStarted.Eagerly, FFeatureStatus.Retrieving) + .flatMapLatest { + (it as? FFeatureStatus.Supported) + ?.featureApi + ?.getSpeed() + ?.map { speedState -> speedState.receiveBytesInSec } + ?: flowOf(0L) + }.stateIn(viewModelScope, SharingStarted.Eagerly, 0L) + + private fun startFeature() { + featureProvider.get() + .onEach { status -> + when (status) { + FFeatureStatus.NotFound -> _state.emit(State.Unsupported) + FFeatureStatus.Retrieving -> { + _state.emit( + State.Uploading( + fullPathOnFlipper = fullPathOnFlipper, + uploaded = 0L, + total = 0L + ) + ) + } + + is FFeatureStatus.Supported -> { + status.featureApi.uploadApi() + .upload( + pathOnFlipper = fullPathOnFlipper.toString(), + fileOnAndroid = fullPathOnDevice, + progressListener = { current, max -> + _state.update { + State.Uploading( + fullPathOnFlipper = fullPathOnFlipper, + uploaded = current, + total = max + ) + } + } + ).onFailure { + error(it) { "#startFeature could not save file $fullPathOnFlipper" } + _state.emit(State.Error) + }.onSuccess { + _state.update { state -> + (state as? State.Uploading)?.let { uploadingState -> + State.Saved( + fullPathOnFlipper = uploadingState.fullPathOnFlipper, + size = uploadingState.total + ) + } ?: state + } + } + } + + FFeatureStatus.Unsupported -> _state.emit(State.Unsupported) + } + }.launchIn(viewModelScope) + } + + init { + startFeature() + } + + sealed interface State { + data object Unsupported : State + data object Error : State + data class Uploading( + val fullPathOnFlipper: Path, + val uploaded: Long, + val total: Long + ) : State { + val progress: Float = when (total) { + 0L -> 0f + else -> (uploaded / total.toFloat()).coerceIn(0f, 1f) + } + } + + data class Saved( + val fullPathOnFlipper: Path, + val size: Long + ) : State + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + @Assisted("fullPathOnFlipper") fullPathOnFlipper: Path, + @Assisted("fullPathOnDevice") fullPathOnDevice: Path, + ): UploadFileViewModel + } +} diff --git a/components/filemngr/listing/api/build.gradle.kts b/components/filemngr/listing/api/build.gradle.kts index 650fa2e89a..0c6d3a8eef 100644 --- a/components/filemngr/listing/api/build.gradle.kts +++ b/components/filemngr/listing/api/build.gradle.kts @@ -7,6 +7,7 @@ android.namespace = "com.flipperdevices.filemanager.listing.api" commonDependencies { implementation(projects.components.core.ui.decompose) + implementation(projects.components.bridge.connection.feature.storage.api) implementation(libs.compose.ui) implementation(libs.decompose) diff --git a/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt index d4aa460065..3a1547cb96 100644 --- a/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt +++ b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt @@ -1,6 +1,7 @@ package com.flipperdevices.filemanager.listing.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.ScreenDecomposeComponent import okio.Path @@ -8,6 +9,8 @@ import okio.Path abstract class FilesDecomposeComponent( componentContext: ComponentContext ) : ScreenDecomposeComponent(componentContext) { + abstract fun onFileChanged(listingItem: ListingItem) + fun interface Factory { @Suppress("LongParameterList") operator fun invoke( 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 2d84975c18..c08575cb60 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 @@ -11,6 +11,7 @@ import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory import com.flipperdevices.filemanager.create.api.CreateFileDecomposeComponent @@ -126,6 +127,10 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( backHandler.register(backCallback) } + override fun onFileChanged(listingItem: ListingItem) { + filesViewModel.onFilesChanged(listOf(listingItem)) + } + @Composable override fun Render() { val multipleFilesPicker = uploadDecomposeComponent.rememberMultipleFilesPicker(path) diff --git a/components/filemngr/main/impl/build.gradle.kts b/components/filemngr/main/impl/build.gradle.kts index 55b2c288c1..23bc3510ef 100644 --- a/components/filemngr/main/impl/build.gradle.kts +++ b/components/filemngr/main/impl/build.gradle.kts @@ -17,6 +17,7 @@ commonDependencies { implementation(projects.components.core.ui.ktx) implementation(projects.components.core.ui.res) + implementation(projects.components.bridge.connection.feature.storage.api) implementation(projects.components.bridge.dao.api) implementation(projects.components.filemngr.uiComponents) diff --git a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt index 64345e4ffa..6ab7ecc0b5 100644 --- a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt +++ b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt @@ -14,6 +14,7 @@ import com.flipperdevices.filemanager.main.impl.model.FileManagerNavigationConfi import com.flipperdevices.filemanager.search.api.SearchDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.findComponentByConfig import com.flipperdevices.ui.decompose.popOr import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -71,6 +72,12 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( componentContext = componentContext, path = config.path, onBack = { navigation.popOr(onBack::invoke) }, + onFileChanged = { item -> + val component = stack.findComponentByConfig( + configClazz = FileManagerNavigationConfig.FileTree::class + ) as? FilesDecomposeComponent + component?.onFileChanged(item) + } ) } } 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 index 40963223cc..f5377e4ff8 100644 --- 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 @@ -6,4 +6,9 @@ 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 }) + + /** + * Max file of size available to edit + */ + const val LIMITED_SIZE_BYTES = 1024L * 1024L // 1MB } From 57d93f67a90cac5d465e19ebd896ccc06abbb126 Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:41:22 +0300 Subject: [PATCH 3/4] Add move-to into new file manager (#997) **Background** Move-to feature allows multiple or single files to be moved inside flipper zero file system. The feature itself contains limited functionality of Listing. Create folders, sort, show hidden files and etc what is often used inside move-to feature of other file managers. **Changes** - Add move-to feature for files - Edit FlipperButton to add loader. Also made color change animated - Increase timeout duration to 30 seconds **Test plan** - Open file manager, try move one file or multiple files - Try the same for folders - Try move parent folder inside parent's child folder and see it's impossible --- CHANGELOG.md | 1 + .../impl/PendingResponseCounter.kt | 2 +- .../bridge/connection/sample/build.gradle.kts | 2 + .../ktx/elements/ComposableFlipperButton.kt | 49 +++- .../api/CreateFileDecomposeComponentImpl.kt | 3 +- .../impl/viewmodel/DownloadViewModel.kt | 1 - .../dialog/CreateFileDialogComposable.kt | 3 +- .../editor/viewmodel/EditFileNameViewModel.kt | 2 +- .../listing/api/FilesDecomposeComponent.kt | 4 + .../listing/api}/model/ExtendedListingItem.kt | 2 +- .../impl/api/FilesDecomposeComponentImpl.kt | 7 + .../composable/ComposableFileListScreen.kt | 4 +- .../impl/composable/LoadedFilesComposable.kt | 2 +- .../modal/BottomSheetOptionsContent.kt | 4 +- .../modal/FileOptionsBottomSheet.kt | 5 +- .../composable/options/BottomBarOptions.kt | 8 +- .../listing/impl/viewmodel/FilesViewModel.kt | 2 +- .../filemngr/main/impl/build.gradle.kts | 1 + .../api/FileManagerDecomposeComponentImpl.kt | 37 +++ .../impl/model/FileManagerNavigationConfig.kt | 12 + .../impl/api/RenameDecomposeComponentImpl.kt | 3 +- .../filemngr/transfer/api/build.gradle.kts | 19 ++ .../api/TransferDecomposeComponent.kt | 35 +++ .../transfer/api/model/TransferType.kt | 5 + .../filemngr/transfer/impl/build.gradle.kts | 65 +++++ .../composeResources/values/strings.xml | 14 ++ .../api/TransferDecomposeComponentImpl.kt | 106 ++++++++ .../composable/ComposableTransferScreen.kt | 121 ++++++++++ .../FullScreenMoveButtonComposable.kt | 34 +++ .../impl/composable/ListOptionsComposable.kt | 73 ++++++ .../impl/composable/MoreIconComposable.kt | 46 ++++ .../impl/composable/TransferAppBar.kt | 32 +++ .../TransferFolderCardListComposable.kt | 50 ++++ .../impl/composable/TransferPathComposable.kt | 32 +++ .../impl/composable/UnsupportedComposable.kt | 13 + .../transfer/impl/viewmodel/FilesViewModel.kt | 226 ++++++++++++++++++ .../impl/viewmodel/OptionsViewModel.kt | 113 +++++++++ .../impl/viewmodel/TransferViewModel.kt | 92 +++++++ .../ui/components/name/NameDialogPreview.kt | 3 +- .../itemcard/FolderCardListComposable.kt | 6 +- .../ui/components/name/NameDialog.kt | 4 +- .../util/serialization/PathSerializer.kt | 1 + settings.gradle.kts | 2 + 43 files changed, 1214 insertions(+), 32 deletions(-) rename components/filemngr/listing/{impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl => api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api}/model/ExtendedListingItem.kt (95%) create mode 100644 components/filemngr/transfer/api/build.gradle.kts create mode 100644 components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/TransferDecomposeComponent.kt create mode 100644 components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/model/TransferType.kt create mode 100644 components/filemngr/transfer/impl/build.gradle.kts create mode 100644 components/filemngr/transfer/impl/src/commonMain/composeResources/values/strings.xml create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/api/TransferDecomposeComponentImpl.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/FullScreenMoveButtonComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ListOptionsComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/MoreIconComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferAppBar.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferFolderCardListComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferPathComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/FilesViewModel.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/OptionsViewModel.kt create mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/TransferViewModel.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index f7afc0244a..8b65cbd275 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 +- [Feature] Add move-to to new file manager - [Refactor] Move rename and file create to separated modules - [Refactor] Improve and refactor new FileManager Editor - [FIX] Migrate url host from metric.flipperdevices.com to metric.flipp.dev diff --git a/components/bridge/connection/feature/lagsdetector/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/seriallagsdetector/impl/PendingResponseCounter.kt b/components/bridge/connection/feature/lagsdetector/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/seriallagsdetector/impl/PendingResponseCounter.kt index 8ee60caafe..5106e3822b 100644 --- a/components/bridge/connection/feature/lagsdetector/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/seriallagsdetector/impl/PendingResponseCounter.kt +++ b/components/bridge/connection/feature/lagsdetector/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/seriallagsdetector/impl/PendingResponseCounter.kt @@ -52,6 +52,6 @@ internal class PendingResponseCounter( } companion object { - internal const val LAGS_FLIPPER_DETECT_TIMEOUT_MS = 10 * 1000L // 10 seconds + internal const val LAGS_FLIPPER_DETECT_TIMEOUT_MS = 30 * 1000L // 30 seconds } } diff --git a/components/bridge/connection/sample/build.gradle.kts b/components/bridge/connection/sample/build.gradle.kts index 7febdd4d8b..f4f02c79fc 100644 --- a/components/bridge/connection/sample/build.gradle.kts +++ b/components/bridge/connection/sample/build.gradle.kts @@ -93,6 +93,8 @@ dependencies { implementation(projects.components.filemngr.rename.impl) implementation(projects.components.filemngr.create.api) implementation(projects.components.filemngr.create.impl) + implementation(projects.components.filemngr.transfer.api) + implementation(projects.components.filemngr.transfer.impl) implementation(projects.components.newfilemanager.api) implementation(projects.components.newfilemanager.impl) diff --git a/components/core/ui/ktx/src/commonMain/kotlin/com/flipperdevices/core/ui/ktx/elements/ComposableFlipperButton.kt b/components/core/ui/ktx/src/commonMain/kotlin/com/flipperdevices/core/ui/ktx/elements/ComposableFlipperButton.kt index 88c80ac61a..e02bf8b864 100644 --- a/components/core/ui/ktx/src/commonMain/kotlin/com/flipperdevices/core/ui/ktx/elements/ComposableFlipperButton.kt +++ b/components/core/ui/ktx/src/commonMain/kotlin/com/flipperdevices/core/ui/ktx/elements/ComposableFlipperButton.kt @@ -1,20 +1,27 @@ package com.flipperdevices.core.ui.ktx.elements +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import com.flipperdevices.core.ui.ktx.clickableRipple import com.flipperdevices.core.ui.ktx.placeholderByLocalProvider import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.core.ui.theme.LocalTypography @Composable @@ -24,27 +31,45 @@ fun ComposableFlipperButton( textPadding: PaddingValues = PaddingValues(vertical = 16.dp, horizontal = 38.dp), onClick: () -> Unit = {}, textStyle: TextStyle = TextStyle(), - enabled: Boolean = true + enabled: Boolean = true, + isLoading: Boolean = false, ) { - val background = if (enabled) { - LocalPallet.current.accentSecond - } else { - LocalPallet.current.flipperDisableButton - } + val background by animateColorAsState( + targetValue = if (enabled && !isLoading) { + LocalPallet.current.accentSecond + } else { + LocalPallet.current.flipperDisableButton + } + ) Box( modifier = modifier .clip(RoundedCornerShape(size = 30.dp)) .placeholderByLocalProvider() .background(background) - .clickableRipple(onClick = onClick), + .clickableRipple(onClick = onClick, enabled = enabled && !isLoading), contentAlignment = Alignment.Center ) { - Text( - modifier = Modifier.padding(textPadding), - text = text, - color = LocalPallet.current.onFlipperButton, - style = LocalTypography.current.buttonB16.merge(textStyle) + AnimatedContent( + targetState = isLoading, + contentAlignment = Alignment.Center, + content = { animatedIsLoading -> + if (animatedIsLoading) { + CircularProgressIndicator( + color = LocalPalletV2.current.action.blue.icon.onColor, + modifier = Modifier.padding(textPadding).size(22.dp), + strokeCap = StrokeCap.Round, + strokeWidth = 2.dp + ) + } else { + Text( + modifier = Modifier.padding(textPadding), + text = text, + color = LocalPallet.current.onFlipperButton, + style = LocalTypography.current.buttonB16.merge(textStyle) + ) + } + } ) } } 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 index 9b2125e47a..f71e52bf5d 100644 --- 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 @@ -84,7 +84,8 @@ class CreateFileDecomposeComponentImpl @AssistedInject constructor( onTextChange = createFileViewModel::onNameChange, onDismissRequest = createFileViewModel::dismiss, onOptionSelect = createFileViewModel::onOptionSelected, - options = localState.options + options = localState.options, + isLoading = localState.isCreating ) } } diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt index 06854b59dc..61c2e0bb89 100644 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt @@ -128,7 +128,6 @@ class DownloadViewModel @Inject constructor( } }.catch { it.printStackTrace() }.launchIn(viewModelScope) _featureJob?.join() - println("DownloadViewModel out of mutex") } } } 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 b240bfd164..2cc1773a71 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 @@ -36,6 +36,7 @@ fun CreateFileDialogComposable( onTextChange = editFileNameViewModel::onChange, onDismissRequest = onDismiss, onOptionSelect = editFileNameViewModel::onOptionSelected, - options = state.options + options = state.options, + isLoading = false ) } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt index 32517b1f76..fa153f903b 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/viewmodel/EditFileNameViewModel.kt @@ -48,7 +48,7 @@ class EditFileNameViewModel @AssistedInject constructor( data class State( val name: String = "", - val isValid: Boolean = false + val isValid: Boolean = false, ) { val options = FileManagerConstants.FILE_EXTENSION_HINTS.toImmutableList() diff --git a/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt index 3a1547cb96..671d8d6736 100644 --- a/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt +++ b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/FilesDecomposeComponent.kt @@ -19,6 +19,7 @@ abstract class FilesDecomposeComponent( path: Path, pathChangedCallback: PathChangedCallback, fileSelectedCallback: FileSelectedCallback, + moveToCallback: MoveToCallback, searchCallback: SearchCallback, ): FilesDecomposeComponent } @@ -38,4 +39,7 @@ abstract class FilesDecomposeComponent( fun interface UploadCallback { fun invoke() } + fun interface MoveToCallback { + fun invoke(fullPaths: List) + } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/model/ExtendedListingItem.kt similarity index 95% rename from components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt rename to components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/model/ExtendedListingItem.kt index 1f3808e03e..b81e44c764 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt +++ b/components/filemngr/listing/api/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/api/model/ExtendedListingItem.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.filemanager.listing.impl.model +package com.flipperdevices.filemanager.listing.api.model import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem 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 c08575cb60..4f58d0750e 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 @@ -44,6 +44,7 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( @Assisted private val pathChangedCallback: PathChangedCallback, @Assisted private val fileSelectedCallback: FileSelectedCallback, @Assisted private val searchCallback: SearchCallback, + @Assisted private val moveToCallback: MoveToCallback, private val storageInfoViewModelFactory: Provider, private val optionsInfoViewModelFactory: Provider, private val deleteFilesViewModelFactory: Provider, @@ -168,6 +169,9 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( .value, onCreate = { type -> createDecomposeComponent.startCreate(path, type) + }, + onMove = { pathsWithType -> + moveToCallback.invoke(pathsWithType.map(PathWithType::fullPath)) } ) FileOptionsBottomSheet( @@ -178,6 +182,9 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( onDownloadFile = downloadDecomposeComponent::download, onRename = { pathWithType -> renameDecomposeComponent.startRename(pathWithType.fullPath, pathWithType.fileType) + }, + onMoveTo = { pathWithType -> + moveToCallback.invoke(listOf(pathWithType.fullPath)) } ) uploadDecomposeComponent.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 f924153377..7e24aab687 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 @@ -43,6 +43,7 @@ fun ComposableFileListScreen( onFileMoreClick: (PathWithType) -> Unit, onCreate: (FileType) -> Unit, onRename: (PathWithType) -> Unit, + onMove: (List) -> Unit, modifier: Modifier = Modifier ) { val canDeleteFiles by deleteFileViewModel.canDeleteFiles.collectAsState() @@ -129,7 +130,8 @@ fun ComposableFileListScreen( filesListState = filesListState, selectionViewModel = selectionViewModel, deleteFileViewModel = deleteFileViewModel, - onRename = onRename + onRename = onRename, + onMove = onMove ) } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt index b6fed028a6..771fd953c2 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/LoadedFilesComposable.kt @@ -10,7 +10,7 @@ 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.core.preference.pb.FileManagerOrientation -import com.flipperdevices.filemanager.listing.impl.model.ExtendedListingItem +import com.flipperdevices.filemanager.listing.api.model.ExtendedListingItem import com.flipperdevices.filemanager.listing.impl.model.PathWithType import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel 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 1d62ca1a85..09396432b7 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 @@ -93,14 +93,14 @@ fun BottomSheetOptionsContent( text = stringResource(FML.string.fml_move_to), painter = painterResource(FR.drawable.ic_move), onClick = onMoveTo, - isEnabled = false + isEnabled = true ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_export), painter = painterResource(FR.drawable.ic_upload), onClick = onExport, - isEnabled = true + isEnabled = fileType == FileType.FILE ) 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 566c78b476..cbd8438b34 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 @@ -20,6 +20,7 @@ fun FileOptionsBottomSheet( deleteFileViewModel: DeleteFilesViewModel, onDownloadFile: (Path, Long) -> Unit, onRename: (PathWithType) -> Unit, + onMoveTo: (PathWithType) -> Unit, modifier: Modifier = Modifier ) { SlotModalBottomSheet( @@ -47,7 +48,9 @@ fun FileOptionsBottomSheet( deleteFileViewModel.tryDelete(pathWithType.fullPath) slotNavigation.dismiss() }, - onMoveTo = {} // todo + onMoveTo = { + onMoveTo.invoke(pathWithType) + } ) } ) 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 1e8a276be7..02669be661 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 @@ -141,6 +141,7 @@ fun FullScreenBottomBarOptions( filesListState: FilesViewModel.State, selectionState: SelectionViewModel.State, onRename: (PathWithType) -> Unit, + onMove: (List) -> Unit, modifier: Modifier = Modifier ) { Box( @@ -156,9 +157,12 @@ fun FullScreenBottomBarOptions( ) { BottomBarOptions( canRename = selectionState.canRename, - onMove = {}, // todo + onMove = { + onMove.invoke(selectionState.selected.toList()) + }, onRename = { - val pathWithType = selectionState.selected.firstOrNull() ?: return@BottomBarOptions + val pathWithType = + selectionState.selected.firstOrNull() ?: return@BottomBarOptions selectionViewModel.toggleMode() onRename.invoke(pathWithType) }, 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 1b3611ac85..4999e4e497 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 @@ -17,7 +17,7 @@ import com.flipperdevices.core.log.error import com.flipperdevices.core.preference.pb.FileManagerSort import com.flipperdevices.core.preference.pb.Settings import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import com.flipperdevices.filemanager.listing.impl.model.ExtendedListingItem +import com.flipperdevices.filemanager.listing.api.model.ExtendedListingItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/components/filemngr/main/impl/build.gradle.kts b/components/filemngr/main/impl/build.gradle.kts index 23bc3510ef..dfcec26fd8 100644 --- a/components/filemngr/main/impl/build.gradle.kts +++ b/components/filemngr/main/impl/build.gradle.kts @@ -22,6 +22,7 @@ commonDependencies { implementation(projects.components.filemngr.uiComponents) implementation(projects.components.filemngr.main.api) + implementation(projects.components.filemngr.transfer.api) implementation(projects.components.filemngr.listing.api) implementation(projects.components.filemngr.upload.api) implementation(projects.components.filemngr.search.api) diff --git a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt index 6ab7ecc0b5..edb444bb17 100644 --- a/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt +++ b/components/filemngr/main/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/main/impl/api/FileManagerDecomposeComponentImpl.kt @@ -12,6 +12,8 @@ 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.transfer.api.TransferDecomposeComponent +import com.flipperdevices.filemanager.transfer.api.model.TransferType import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.findComponentByConfig @@ -28,6 +30,7 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( private val filesDecomposeComponentFactory: FilesDecomposeComponent.Factory, private val searchDecomposeComponentFactory: SearchDecomposeComponent.Factory, private val editorDecomposeComponentFactory: FileManagerEditorDecomposeComponent.Factory, + private val transferDecomposeComponentFactory: TransferDecomposeComponent.Factory ) : FileManagerDecomposeComponent(), ComponentContext by componentContext { @@ -39,6 +42,7 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( childFactory = ::child, ) + @Suppress("LongMethod") private fun child( config: FileManagerNavigationConfig, componentContext: ComponentContext @@ -54,6 +58,15 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( fileSelectedCallback = { navigation.pushNew(FileManagerNavigationConfig.Edit(it)) }, + moveToCallback = { fullPaths -> + navigation.pushNew( + FileManagerNavigationConfig.Transfer( + path = config.path, + transferType = TransferType.MOVE, + fullPathToMove = fullPaths + ) + ) + }, searchCallback = { navigation.pushNew(FileManagerNavigationConfig.Search(config.path)) }, ) } @@ -80,5 +93,29 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( } ) } + + is FileManagerNavigationConfig.Transfer -> { + transferDecomposeComponentFactory.invoke( + componentContext = componentContext, + param = TransferDecomposeComponent.Param( + path = config.path, + transferType = config.transferType, + fullPathToMove = config.fullPathToMove + ), + onBack = { navigation.popOr(onBack::invoke) }, + onMoved = { path -> + navigation.replaceAll(FileManagerNavigationConfig.FileTree(path)) + }, + onPathChange = { path -> + navigation.replaceCurrent( + FileManagerNavigationConfig.Transfer( + path = path, + transferType = config.transferType, + fullPathToMove = config.fullPathToMove + ) + ) + } + ) + } } } 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 9e368eadc6..cd3645f00a 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,6 +1,7 @@ package com.flipperdevices.filemanager.main.impl.model import androidx.compose.runtime.Stable +import com.flipperdevices.filemanager.transfer.api.model.TransferType import com.flipperdevices.filemanager.util.serialization.PathSerializer import kotlinx.serialization.Serializable import okio.Path @@ -27,6 +28,17 @@ sealed interface FileManagerNavigationConfig { val path: Path ) : FileManagerNavigationConfig + @Serializable + data class Transfer( + @Serializable(with = PathSerializer::class) + val path: Path, + val transferType: TransferType, + val fullPathToMove: List< + @Serializable(with = PathSerializer::class) + Path + >, + ) : FileManagerNavigationConfig + companion object { val DefaultFileTree: FileTree get() = FileTree("/".toPath()) 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 index 658566b7bc..46f2a76896 100644 --- 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 @@ -70,7 +70,8 @@ class RenameDecomposeComponentImpl @AssistedInject constructor( onTextChange = renameViewModel::onNameChange, onDismissRequest = renameViewModel::dismiss, onOptionSelect = renameViewModel::onOptionSelected, - options = localState.options + options = localState.options, + isLoading = localState.isRenaming ) } } diff --git a/components/filemngr/transfer/api/build.gradle.kts b/components/filemngr/transfer/api/build.gradle.kts new file mode 100644 index 0000000000..1e9e8c64b4 --- /dev/null +++ b/components/filemngr/transfer/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") +} + +android.namespace = "com.flipperdevices.filemanager.transfer.api" + +commonDependencies { + implementation(projects.components.core.ui.decompose) + implementation(projects.components.deeplink.api) + + implementation(projects.components.filemngr.listing.api) + + implementation(libs.compose.ui) + implementation(libs.decompose) + + implementation(libs.okio) +} diff --git a/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/TransferDecomposeComponent.kt b/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/TransferDecomposeComponent.kt new file mode 100644 index 0000000000..a72be6b6e8 --- /dev/null +++ b/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/TransferDecomposeComponent.kt @@ -0,0 +1,35 @@ +package com.flipperdevices.filemanager.transfer.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.filemanager.transfer.api.model.TransferType +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import okio.Path + +abstract class TransferDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + param: Param, + onBack: DecomposeOnBackParameter, + onMoved: MovedCallback, + onPathChange: PathChangedCallback, + ): TransferDecomposeComponent + } + + fun interface MovedCallback { + fun invoke(path: Path) + } + + fun interface PathChangedCallback { + fun invoke(path: Path) + } + + data class Param( + val path: Path, + val transferType: TransferType, + val fullPathToMove: List, + ) +} diff --git a/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/model/TransferType.kt b/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/model/TransferType.kt new file mode 100644 index 0000000000..dc7aa0b2b7 --- /dev/null +++ b/components/filemngr/transfer/api/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/api/model/TransferType.kt @@ -0,0 +1,5 @@ +package com.flipperdevices.filemanager.transfer.api.model + +enum class TransferType { + MOVE +} diff --git a/components/filemngr/transfer/impl/build.gradle.kts b/components/filemngr/transfer/impl/build.gradle.kts new file mode 100644 index 0000000000..f5220ada90 --- /dev/null +++ b/components/filemngr/transfer/impl/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.filemanager.transfer.impl" + +commonDependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + implementation(projects.components.core.progress) + 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.storage) + + implementation(projects.components.bridge.dao.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.connection.orchestrator.api) + + implementation(projects.components.bridge.dao.api) + + implementation(projects.components.filemngr.uiComponents) + implementation(projects.components.filemngr.transfer.api) + implementation(projects.components.filemngr.listing.api) + implementation(projects.components.filemngr.create.api) + + implementation(projects.components.deeplink.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + implementation(libs.compose.activity) + 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) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/transfer/impl/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..2f3bf2589f --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,14 @@ + + + Create Folder + List + Grid + Sort by Default + Sort by Size + Show Hidden Files + + %1$s items + + Moving + Move Here + \ No newline at end of file diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/api/TransferDecomposeComponentImpl.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/api/TransferDecomposeComponentImpl.kt new file mode 100644 index 0000000000..5bee7d3215 --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/api/TransferDecomposeComponentImpl.kt @@ -0,0 +1,106 @@ +package com.flipperdevices.filemanager.transfer.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.decompose.childContext +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.filemanager.create.api.CreateFileDecomposeComponent +import com.flipperdevices.filemanager.transfer.api.TransferDecomposeComponent +import com.flipperdevices.filemanager.transfer.impl.composable.ComposableTransferScreen +import com.flipperdevices.filemanager.transfer.impl.viewmodel.FilesViewModel +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel +import com.flipperdevices.filemanager.transfer.impl.viewmodel.TransferViewModel +import com.flipperdevices.ui.decompose.DecomposeOnBackParameter +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.gulya.anvil.assisted.ContributesAssistedFactory +import javax.inject.Provider + +@Suppress("LongParameterList") +@ContributesAssistedFactory(AppGraph::class, TransferDecomposeComponent.Factory::class) +class TransferDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + @Assisted private val param: Param, + @Assisted private val onBack: DecomposeOnBackParameter, + @Assisted private val onMoved: MovedCallback, + @Assisted private val onPathChange: PathChangedCallback, + filesViewModelFactory: FilesViewModel.Factory, + transferViewModelProvider: Provider, + optionsViewModelProvider: Provider, + createFileDecomposeComponentFactory: CreateFileDecomposeComponent.Factory, +) : TransferDecomposeComponent(componentContext) { + private val filesViewModel = instanceKeeper.getOrCreate("files_${param.path}") { + filesViewModelFactory.invoke(path = param.path) + } + private val transferViewModel = instanceKeeper.getOrCreate("transfer_${param.path}") { + transferViewModelProvider.get() + } + private val optionsViewModel = instanceKeeper.getOrCreate("options_${param.path}") { + optionsViewModelProvider.get() + } + private val createFileDecomposeComponent = createFileDecomposeComponentFactory.invoke( + componentContext = childContext("createfolder_${param.path}"), + createCallback = filesViewModel::onFolderCreated + ) + + // Folder should not be a subfolder of movable folder + // Folder should be the same as movable folder's parent + private val canMoveHere = param.fullPathToMove + .all { path -> !param.path.toString().startsWith(path.toString()) } + .and(param.fullPathToMove.all { path -> param.path != path.parent }) + + private val backCallback = BackCallback { + val parent = param.path.parent + if (transferViewModel.state.value is TransferViewModel.State.Moving) return@BackCallback + if (parent == null) { + onBack.invoke() + } else { + onPathChange.invoke(parent) + } + } + + init { + backHandler.register(backCallback) + } + + @Composable + override fun Render() { + LaunchedEffect(transferViewModel) { + transferViewModel.state + .filterIsInstance() + .onEach { state -> onMoved.invoke(state.targetDir) } + .launchIn(this) + } + + val optionsState by optionsViewModel.state.collectAsState() + val state by filesViewModel.state.collectAsState() + val transferState by transferViewModel.state.collectAsState() + val isMoving = transferState is TransferViewModel.State.Moving + ComposableTransferScreen( + optionsState = optionsState, + state = state, + isMoving = isMoving, + canMoveHere = canMoveHere, + param = param, + onBack = onBack::invoke, + onOptionsAction = optionsViewModel::onAction, + onCreateFolder = { createFileDecomposeComponent.startCreateFolder(param.path) }, + onPathChange = onPathChange::invoke, + onMoveStart = { + transferViewModel.move( + oldPaths = param.fullPathToMove, + targetDir = param.path + ) + }, + ) + createFileDecomposeComponent.Render() + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt new file mode 100644 index 0000000000..1b72819e70 --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt @@ -0,0 +1,121 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.preference.pb.FileManagerOrientation +import com.flipperdevices.filemanager.transfer.api.TransferDecomposeComponent.Param +import com.flipperdevices.filemanager.transfer.impl.viewmodel.FilesViewModel +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel +import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardPlaceholderComposable +import okio.Path + +@Suppress("LongMethod") +@Composable +fun ComposableTransferScreen( + optionsState: OptionsViewModel.State, + state: FilesViewModel.State, + isMoving: Boolean, + canMoveHere: Boolean, + param: Param, + onBack: () -> Unit, + onOptionsAction: (OptionsViewModel.Action) -> Unit, + onCreateFolder: () -> Unit, + onPathChange: (Path) -> Unit, + onMoveStart: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + TransferAppBar( + transferType = param.transferType, + onBack = onBack, + optionsState = optionsState, + onOptionsAction = onOptionsAction, + onCreateFolder = onCreateFolder + ) + } + ) { contentPaddings -> + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(14.dp), + columns = when (optionsState.orientation) { + FileManagerOrientation.GRID -> GridCells.Fixed(2) + is FileManagerOrientation.Unrecognized, + FileManagerOrientation.LIST -> GridCells.Fixed(1) + } + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + TransferPathComposable( + isMoving = isMoving, + path = param.path, + onPathChange = onPathChange + ) + } + + when (val localState = state) { + FilesViewModel.State.CouldNotListPath -> { + item( + span = { GridItemSpan(maxLineSpan) }, + content = { Box(Modifier.fillMaxSize().background(Color.Red)) } + ) + } + + is FilesViewModel.State.Loaded -> { + items(localState.files) { item -> + TransferFolderCardListComposable( + modifier = Modifier.animateItem(), + fullDirPath = param.path, + item = item, + onPathChange = onPathChange, + isMoving = isMoving + ) + } + item( + span = { GridItemSpan(maxLineSpan) }, + content = { Box(Modifier.height(32.dp)) } + ) + } + + FilesViewModel.State.Loading -> { + items(count = 6) { + FolderCardPlaceholderComposable( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + orientation = optionsState.orientation, + ) + } + } + + FilesViewModel.State.Unsupported -> { + item( + span = { GridItemSpan(maxLineSpan) }, + content = { UnsupportedComposable() } + ) + } + } + } + FullScreenMoveButtonComposable( + isLoading = isMoving, + isEnabled = !isMoving && canMoveHere, + onClick = onMoveStart + ) + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/FullScreenMoveButtonComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/FullScreenMoveButtonComposable.kt new file mode 100644 index 0000000000..32d4d1b14c --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/FullScreenMoveButtonComposable.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.flipperdevices.core.ui.ktx.elements.ComposableFlipperButton +import flipperapp.components.filemngr.transfer.impl.generated.resources.Res +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_move_button +import org.jetbrains.compose.resources.stringResource + +@Composable +fun FullScreenMoveButtonComposable( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + content = { + ComposableFlipperButton( + text = stringResource(Res.string.fmt_move_button), + modifier = Modifier.fillMaxWidth(), + isLoading = isLoading, + enabled = isEnabled, + onClick = onClick + ) + } + ) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ListOptionsComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ListOptionsComposable.kt new file mode 100644 index 0000000000..22779750f6 --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ListOptionsComposable.kt @@ -0,0 +1,73 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel.Action +import com.flipperdevices.filemanager.ui.components.dropdown.IconDropdownItem +import com.flipperdevices.filemanager.ui.components.dropdown.RadioDropdownItem +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_create_folder +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_display_grid +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_display_list +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_show_hidden +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_sort_default +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_otp_sort_size +import flipperapp.components.filemngr.ui_components.generated.resources.ic_create_fodler +import flipperapp.components.filemngr.ui_components.generated.resources.ic_grid +import flipperapp.components.filemngr.ui_components.generated.resources.ic_list +import flipperapp.components.filemngr.ui_components.generated.resources.ic_sort_default +import flipperapp.components.filemngr.ui_components.generated.resources.ic_sort_size +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.transfer.impl.generated.resources.Res as FMT +import flipperapp.components.filemngr.ui_components.generated.resources.Res as FR + +@Composable +fun ListOptionsDropDown( + isVisible: Boolean, + isHiddenFilesVisible: Boolean, + onAction: (Action) -> Unit, + onCreateFolderClick: () -> Unit, + modifier: Modifier = Modifier +) { + DropdownMenu( + modifier = modifier, + expanded = isVisible, + onDismissRequest = { onAction.invoke(Action.ToggleMenu) }, + ) { + IconDropdownItem( + text = stringResource(FMT.string.fmt_otp_create_folder), + painter = painterResource(FR.drawable.ic_create_fodler), + onClick = onCreateFolderClick + ) + Divider() + IconDropdownItem( + text = stringResource(FMT.string.fmt_otp_display_list), + painter = painterResource(FR.drawable.ic_list), + onClick = { onAction.invoke(Action.DisplayList) } + ) + IconDropdownItem( + text = stringResource(FMT.string.fmt_otp_display_grid), + painter = painterResource(FR.drawable.ic_grid), + onClick = { onAction.invoke(Action.DisplayGrid) } + ) + Divider() + IconDropdownItem( + text = stringResource(FMT.string.fmt_otp_sort_default), + painter = painterResource(FR.drawable.ic_sort_default), + onClick = { onAction.invoke(Action.SortByDefault) } + ) + IconDropdownItem( + text = stringResource(FMT.string.fmt_otp_sort_size), + painter = painterResource(FR.drawable.ic_sort_size), + onClick = { onAction.invoke(Action.SortBySize) } + ) + Divider() + RadioDropdownItem( + text = stringResource(FMT.string.fmt_otp_show_hidden), + onClick = { onAction.invoke(Action.ToggleHidden) }, + selected = isHiddenFilesVisible + ) + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/MoreIconComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/MoreIconComposable.kt new file mode 100644 index 0000000000..155c9649da --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/MoreIconComposable.kt @@ -0,0 +1,46 @@ +package com.flipperdevices.filemanager.transfer.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.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel.Action +import flipperapp.components.core.ui.res.generated.resources.ic_more_points +import org.jetbrains.compose.resources.painterResource +import flipperapp.components.core.ui.res.generated.resources.Res as CoreUiRes + +@Composable +fun MoreIconComposable( + optionsState: OptionsViewModel.State, + onAction: (Action) -> Unit, + onCreateFolderClick: () -> Unit, + modifier: Modifier = Modifier + +) { + Box(modifier = modifier) { + Icon( + modifier = Modifier + .padding(end = 14.dp) + .size(24.dp) + .clickableRipple(onClick = { onAction.invoke(Action.ToggleMenu) }), + painter = painterResource(CoreUiRes.drawable.ic_more_points), + contentDescription = null, + tint = LocalPalletV2.current.icon.blackAndWhite.blackOnColor + ) + ListOptionsDropDown( + isVisible = optionsState.isVisible, + onAction = onAction, + isHiddenFilesVisible = optionsState.isHiddenFilesVisible, + onCreateFolderClick = { + onAction.invoke(Action.ToggleMenu) + onCreateFolderClick.invoke() + }, + ) + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferAppBar.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferAppBar.kt new file mode 100644 index 0000000000..fbc3fe16e8 --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferAppBar.kt @@ -0,0 +1,32 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.runtime.Composable +import com.flipperdevices.core.ui.ktx.OrangeAppBar +import com.flipperdevices.filemanager.transfer.api.model.TransferType +import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_appbar_title_moving +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.transfer.impl.generated.resources.Res as FMT + +@Composable +fun TransferAppBar( + transferType: TransferType, + onBack: () -> Unit, + optionsState: OptionsViewModel.State, + onOptionsAction: (OptionsViewModel.Action) -> Unit, + onCreateFolder: () -> Unit +) { + OrangeAppBar( + title = when (transferType) { + TransferType.MOVE -> stringResource(FMT.string.fmt_appbar_title_moving) + }, + onBack = onBack::invoke, + endBlock = { + MoreIconComposable( + optionsState = optionsState, + onAction = onOptionsAction, + onCreateFolderClick = onCreateFolder, + ) + } + ) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferFolderCardListComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferFolderCardListComposable.kt new file mode 100644 index 0000000000..97ed6ab767 --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferFolderCardListComposable.kt @@ -0,0 +1,50 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.runtime.Composable +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.listing.api.model.ExtendedListingItem +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 flipperapp.components.filemngr.transfer.impl.generated.resources.Res +import flipperapp.components.filemngr.transfer.impl.generated.resources.fmt_items_in_folder +import okio.Path +import org.jetbrains.compose.resources.stringResource + +@Composable +fun TransferFolderCardListComposable( + fullDirPath: Path, + item: ExtendedListingItem, + isMoving: Boolean, + onPathChange: (Path) -> Unit, + modifier: Modifier = Modifier +) { + FolderCardListComposable( + modifier = modifier, + painter = item.asListingItem().asPainter(), + iconTint = item.asListingItem().asTint(), + title = item.itemName, + subtitle = when (item) { + is ExtendedListingItem.File -> item.size.toFormattedSize() + is ExtendedListingItem.Folder -> stringResource( + resource = Res.string.fmt_items_in_folder, + item.itemsCount.toString() + ) + }, + isSubtitleLoading = when (item) { + is ExtendedListingItem.File -> false + is ExtendedListingItem.Folder -> item.itemsCount == null + }, + selectionState = ItemUiSelectionState.NONE, + onClick = onClick@{ + if (item.itemType != FileType.DIR) return@onClick + if (isMoving) return@onClick + onPathChange.invoke(fullDirPath.resolve(item.itemName)) + }, + onCheckChange = null, + onMoreClick = null + ) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferPathComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferPathComposable.kt new file mode 100644 index 0000000000..3c475059ac --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/TransferPathComposable.kt @@ -0,0 +1,32 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flipperdevices.filemanager.ui.components.path.PathComposable +import okio.Path + +@Composable +fun TransferPathComposable( + isMoving: Boolean, + path: Path, + onPathChange: (Path) -> Unit, + modifier: Modifier = Modifier +) { + PathComposable( + path = path, + onRootPathClick = onRootPathClick@{ + if (isMoving) return@onRootPathClick + path.root?.run(onPathChange) + }, + onPathClick = onPathClick@{ clickedPath -> + if (isMoving) return@onPathClick + onPathChange.invoke(clickedPath) + }, + modifier = modifier + .fillMaxWidth() + .padding(14.dp) + ) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt new file mode 100644 index 0000000000..09cf387bce --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt @@ -0,0 +1,13 @@ +package com.flipperdevices.filemanager.transfer.impl.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun UnsupportedComposable(modifier: Modifier = Modifier) { + Box(modifier.fillMaxSize().background(Color.Magenta)) +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/FilesViewModel.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/FilesViewModel.kt new file mode 100644 index 0000000000..b4af9c97dc --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/FilesViewModel.kt @@ -0,0 +1,226 @@ +package com.flipperdevices.filemanager.transfer.impl.viewmodel + +import androidx.datastore.core.DataStore +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.toThrowableFlow +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.preference.pb.FileManagerSort +import com.flipperdevices.core.preference.pb.Settings +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.filemanager.listing.api.model.ExtendedListingItem +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet +import okio.Path +import okio.Path.Companion.toPath + +class FilesViewModel @AssistedInject constructor( + private val featureProvider: FFeatureProvider, + @Assisted private val path: Path, + private val settingsDataStore: DataStore +) : DecomposeViewModel(), LogTagProvider { + override val TAG = "FilesViewModel" + + private val _state = MutableStateFlow(State.Loading) + val state = combine( + flow = settingsDataStore.data, + flow2 = _state, + transform = { settings, state -> + when (state) { + State.CouldNotListPath -> state + State.Loading -> state + State.Unsupported -> state + is State.Loaded -> { + state.copy( + files = state.files + .filter { + if (settings.show_hidden_files_on_flipper) { + true + } else { + !it.path.name.startsWith(".") + } + } + .sortedByDescending { + when (settings.file_manager_sort) { + is FileManagerSort.Unrecognized, + FileManagerSort.DEFAULT -> null + + FileManagerSort.SIZE -> { + when (it) { + is ExtendedListingItem.File -> it.size + // The default size for folder is 0 + // Here's placed 0 so sort works as on flipper + is ExtendedListingItem.Folder -> 0 + } + } + } + } + .toImmutableList() + ) + } + } + } + ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + + private fun ListingItem.toExtended(): ExtendedListingItem { + return when (fileType) { + FileType.DIR -> { + ExtendedListingItem.Folder( + path = fileName.toPath(), + itemsCount = null + ) + } + + null, FileType.FILE -> { + ExtendedListingItem.File( + path = fileName.toPath(), + size = size + ) + } + } + } + + fun onFolderCreated(listingItem: ListingItem) { + _state.update { state -> + (state as? State.Loaded)?.let { loadedState -> + val newItems = loadedState.files + .filter { it.path.name != listingItem.fileName } + .plus(listingItem.toExtended()) + .toImmutableList() + loadedState.copy(files = newItems) + } ?: state + } + } + + private suspend fun updateSubfoldersCount( + items: List, + listingApi: FListingStorageApi + ) { + items + .filterIsInstance() + .filter { directory -> directory.itemsCount == null } + .onEach { directory -> + _state.update { state -> + val loadedState = (state as? State.Loaded) + if (loadedState == null) { + error { "#updateFiles state changed during update" } + return@update state + } + val newList = loadedState.files.toMutableList() + val i = newList.indexOfFirst { item -> item == directory } + if (i == -1) { + error { "#updateFiles could not find item in list" } + return@update loadedState + } + val itemsCount = listingApi.ls(path.resolve(directory.path).toString()) + .getOrNull() + .orEmpty() + .size + val updatedDirectory = directory.copy(itemsCount = itemsCount) + newList[i] = updatedDirectory + loadedState.copy(files = newList.toImmutableList()) + } + } + } + + private suspend fun listFiles(listingApi: FListingStorageApi) { + listingApi.lsFlow(path.toString()) + .toThrowableFlow() + .catch { _state.emit(State.CouldNotListPath) } + .map { items -> items.map { item -> item.toExtended() } } + .onEach { files -> + _state.updateAndGet { state -> + when (state) { + is State.Loaded -> { + state.copy(state.files.plus(files).toImmutableList()) + } + + else -> { + State.Loaded(files.toImmutableList()) + } + } + } + }.launchIn(viewModelScope) + } + + private fun startListFiles() { + featureProvider + .get() + .onEach { featureStatus -> + when (featureStatus) { + FFeatureStatus.Unsupported, + FFeatureStatus.NotFound -> { + _state.emit(State.Unsupported) + } + + is FFeatureStatus.Supported -> { + listFiles(featureStatus.featureApi.listingApi()) + } + + FFeatureStatus.Retrieving -> { + _state.emit(State.Loading) + } + } + } + .launchIn(viewModelScope) + } + + private fun startSubfolderCountUpdater() { + combine( + flow = featureProvider + .get() + .filterIsInstance>(), + flow2 = state + .filterIsInstance() + .distinctUntilChangedBy { it.files.size }, + transform = { feature, state -> + updateSubfoldersCount( + items = state.files, + listingApi = feature.featureApi.listingApi() + ) + } + ).launchIn(viewModelScope) + } + + init { + startListFiles() + startSubfolderCountUpdater() + } + + sealed interface State { + data object Loading : State + data object Unsupported : State + data object CouldNotListPath : State + data class Loaded( + val files: ImmutableList, + ) : State + } + + @AssistedFactory + fun interface Factory { + operator fun invoke( + path: Path + ): FilesViewModel + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/OptionsViewModel.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/OptionsViewModel.kt new file mode 100644 index 0000000000..190e7d602a --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/OptionsViewModel.kt @@ -0,0 +1,113 @@ +package com.flipperdevices.filemanager.transfer.impl.viewmodel + +import androidx.datastore.core.DataStore +import com.flipperdevices.core.preference.pb.FileManagerOrientation +import com.flipperdevices.core.preference.pb.FileManagerSort +import com.flipperdevices.core.preference.pb.Settings +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +class OptionsViewModel @Inject constructor( + private val settingsDataStore: DataStore +) : DecomposeViewModel() { + private val _state = MutableStateFlow( + State( + // need runBlocking to get rid of animation change from list to grid + orientation = runBlocking { settingsDataStore.data.first().file_manager_orientation } + ) + ) + val state = _state.asStateFlow() + + fun toggleMenu() { + _state.update { it.copy(isVisible = !it.isVisible) } + } + + private fun setHiddenFiles(isHiddenFilesVisible: Boolean) { + toggleMenu() + viewModelScope.launch { + settingsDataStore.updateData { it.copy(show_hidden_files_on_flipper = isHiddenFilesVisible) } + } + } + + fun toggleHiddenFiles() { + setHiddenFiles(!state.value.isHiddenFilesVisible) + } + + private fun setOrientation(orientation: FileManagerOrientation) { + toggleMenu() + viewModelScope.launch { + settingsDataStore.updateData { it.copy(file_manager_orientation = orientation) } + } + } + + fun setListOrientation() { + setOrientation(FileManagerOrientation.LIST) + } + + fun setGridOrientation() { + setOrientation(FileManagerOrientation.GRID) + } + + private fun setSort(sort: FileManagerSort) { + toggleMenu() + viewModelScope.launch { + settingsDataStore.updateData { it.copy(file_manager_sort = sort) } + } + } + + fun setDefaultSort() { + setSort(FileManagerSort.DEFAULT) + } + + fun setSizeSort() { + setSort(FileManagerSort.SIZE) + } + + fun onAction(action: Action) { + when (action) { + Action.DisplayGrid -> setGridOrientation() + Action.DisplayList -> setListOrientation() + Action.SortByDefault -> setDefaultSort() + Action.SortBySize -> setSizeSort() + Action.ToggleHidden -> toggleHiddenFiles() + Action.ToggleMenu -> toggleMenu() + } + } + + init { + settingsDataStore.data + .onEach { settings -> + _state.update { state -> + state.copy( + orientation = settings.file_manager_orientation, + sortType = settings.file_manager_sort, + isHiddenFilesVisible = settings.show_hidden_files_on_flipper + ) + } + }.launchIn(viewModelScope) + } + + data class State( + val isVisible: Boolean = false, + val isHiddenFilesVisible: Boolean = false, + val orientation: FileManagerOrientation = FileManagerOrientation.LIST, + val sortType: FileManagerSort = FileManagerSort.DEFAULT + ) + + sealed interface Action { + data object ToggleMenu : Action + data object DisplayGrid : Action + data object DisplayList : Action + data object SortBySize : Action + data object SortByDefault : Action + data object ToggleHidden : Action + } +} diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/TransferViewModel.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/TransferViewModel.kt new file mode 100644 index 0000000000..a8426e49fc --- /dev/null +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/viewmodel/TransferViewModel.kt @@ -0,0 +1,92 @@ +package com.flipperdevices.filemanager.transfer.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.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okio.Path +import javax.inject.Inject + +class TransferViewModel @Inject constructor( + private val featureProvider: FFeatureProvider, +) : DecomposeViewModel(), LogTagProvider { + override val TAG = "FilesViewModel" + + private val _state = MutableStateFlow(State.Pending) + val state = _state.asStateFlow() + + private val featureMutex = Mutex() + private var featureJob: Job? = null + private var moveMutex = Mutex() + private var moveJob: Job? = null + + private suspend fun move( + uploadApi: FFileUploadApi, + oldPaths: List, + targetDir: Path + ) { + oldPaths.map { oldPath -> + val targetPath = targetDir.resolve(oldPath.name) + uploadApi.move( + oldPath = oldPath, + newPath = targetPath + ).onFailure { + error(it) { "#onFinish could not move file $oldPaths -> $targetPath" } + } + }.firstOrNull { result -> result.isSuccess }?.onSuccess { + _state.emit(State.Moved(targetDir)) + } + } + + fun move(oldPaths: List, targetDir: Path) { + viewModelScope.launch { + featureJob?.cancelAndJoin() + featureMutex.withLock { + featureJob = featureProvider.get() + .onEach { status -> + moveJob?.cancelAndJoin() + moveMutex.withLock { + when (status) { + FFeatureStatus.NotFound -> _state.emit(State.Unsupported) + FFeatureStatus.Retrieving -> _state.emit(State.Moving) + is FFeatureStatus.Supported -> { + moveJob = viewModelScope.launch { + _state.emit(State.Moving) + move( + uploadApi = status.featureApi.uploadApi(), + oldPaths = oldPaths, + targetDir = targetDir + ) + } + } + + FFeatureStatus.Unsupported -> _state.emit(State.Unsupported) + } + moveJob?.join() + } + }.launchIn(viewModelScope) + featureJob?.join() + } + } + } + + sealed interface State { + data object Unsupported : State + data object Pending : State + data object Moving : State + data class Moved(val targetDir: Path) : State + } +} diff --git a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialogPreview.kt b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialogPreview.kt index 10a138cf2b..eb18b53b08 100644 --- a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialogPreview.kt +++ b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/name/NameDialogPreview.kt @@ -46,7 +46,8 @@ private fun NameDialogPreview() { needShowOptions = File(value).extension.isBlank(), isError = File(value).extension.isBlank(), onFinish = { }, - isEnabled = true + isEnabled = true, + isLoading = false ) } } diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt index fda29be805..2290f5b918 100644 --- a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/itemcard/FolderCardListComposable.kt @@ -109,9 +109,9 @@ fun FolderCardListComposable( isSubtitleLoading: Boolean, selectionState: ItemUiSelectionState, onClick: () -> Unit, - onCheckChange: (Boolean) -> Unit, - onMoreClick: () -> Unit, modifier: Modifier = Modifier, + onCheckChange: ((Boolean) -> Unit)? = null, + onMoreClick: (() -> Unit)? = null, iconTint: Color = Color.Unspecified, showEndBox: Boolean = true ) { @@ -148,7 +148,7 @@ fun FolderCardListComposable( } } - if (showEndBox) { + if (showEndBox && onCheckChange != null && onMoreClick != null) { ItemCardEndBox( selectionState = selectionState, onCheckChange = onCheckChange, 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 eabecf68a5..6aa7bb2dc9 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 @@ -37,6 +37,7 @@ fun NameDialog( needShowOptions: Boolean, isError: Boolean, isEnabled: Boolean, + isLoading: Boolean, onTextChange: (String) -> Unit, options: ImmutableList, onOptionSelect: (index: Int) -> Unit, @@ -84,7 +85,8 @@ fun NameDialog( modifier = Modifier.fillMaxWidth(), textPadding = PaddingValues(vertical = 12.dp), onClick = onFinish, - enabled = isEnabled + enabled = isEnabled, + isLoading = isLoading ) } } diff --git a/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt index 017237d0de..2efa4a8d6d 100644 --- a/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt +++ b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt @@ -1,6 +1,7 @@ package com.flipperdevices.filemanager.util.serialization import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor diff --git a/settings.gradle.kts b/settings.gradle.kts index e7fa5b123e..e9f993d972 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -108,6 +108,8 @@ include( ":components:filemngr:rename:impl", ":components:filemngr:create:api", ":components:filemngr:create:impl", + ":components:filemngr:transfer:api", + ":components:filemngr:transfer:impl", ":components:core:di", ":components:core:ktx", From 30d085b7bbcb81b9ce96fff37081c2d96575ce01 Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:59:31 +0300 Subject: [PATCH 4/4] Fix new file manager issues (#998) **Background** This PR introduces fixes for small variousnew file manager issues **Changes** - Fix download file - Share ProgressScreen between modules - Fix animateions on ProgressScreen - Add temporary error screen(until errors design is completed) - Fix selection after item deleted **Test plan** - Try use new file manager with all it's functionality --- CHANGELOG.md | 1 + .../core/share/AndroidShareHelper.kt | 6 +- .../api/DownloadDecomposeComponent.kt | 7 +- .../download/model/DownloadableFile.kt | 8 ++ .../filemngr/download/impl/build.gradle.kts | 1 + .../api/DownloadDecomposeComponentImpl.kt | 15 +-- .../impl/composable/DownloadingComposable.kt | 69 ++++------ .../impl/composable/InProgressComposable.kt | 111 ---------------- .../download/impl/model/DownloadableFile.kt | 8 -- .../impl/viewmodel/DownloadViewModel.kt | 4 +- .../editor/composable/ContentPreview.kt | 41 ------ .../composeResources/values/strings.xml | 4 +- .../api/FileDownloadDecomposeComponent.kt | 33 +++-- .../api/UploadFileDecomposeComponent.kt | 15 +-- .../content/EditorLoadingContent.kt | 40 ------ .../editor/composable/content/ErrorContent.kt | 15 --- .../composable/content/TooBigContent.kt | 22 ---- .../download/InProgressComposable.kt | 93 ------------- .../download/UploadingComposable.kt | 69 +++------- .../options/BottomBarOptionsPreview.kt | 5 +- .../impl/api/FilesDecomposeComponentImpl.kt | 24 +++- .../composable/ComposableFileListScreen.kt | 17 ++- .../composable/LaunchedEventsComposable.kt | 4 +- .../modal/FileOptionsBottomSheet.kt | 6 +- .../composable/options/BottomBarOptions.kt | 23 +++- .../options/VerticalTextIconButton.kt | 9 +- .../impl/viewmodel/SelectionViewModel.kt | 16 +++ .../composable/ComposableFilesSearchScreen.kt | 3 +- .../composable/NoListingFeatureComposable.kt | 15 --- .../composable/ComposableTransferScreen.kt | 8 +- .../impl/composable/UnsupportedComposable.kt | 13 -- .../error/ErrorContentComposablePreview.kt | 18 +++ ...FileTransferFullScreenComposablePreview.kt | 22 ++++ .../composeResources/values/strings.xml | 7 + .../error/ErrorContentComposable.kt | 76 +++++++++++ .../error/UnknownErrorComposable.kt | 21 +++ .../error/UnsupportedErrorComposable.kt | 21 +++ .../FileTransferFullScreenComposable.kt | 70 ++++++++++ .../transfer/InProgressComposable.kt | 107 +++++++++++++++ .../composable/InProgressComposablePreview.kt | 27 ---- .../impl/composable/InProgressComposable.kt | 124 ------------------ .../impl/composable/UploadingComposable.kt | 87 ++++++------ .../util/serialization/PathSerializer.kt | 1 - 43 files changed, 562 insertions(+), 724 deletions(-) create mode 100644 components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/model/DownloadableFile.kt delete mode 100644 components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/InProgressComposable.kt delete mode 100644 components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/model/DownloadableFile.kt delete mode 100644 components/filemngr/editor/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/editor/composable/ContentPreview.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/EditorLoadingContent.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/ErrorContent.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/TooBigContent.kt delete mode 100644 components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt delete mode 100644 components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt delete mode 100644 components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt create mode 100644 components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposablePreview.kt create mode 100644 components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposablePreview.kt create mode 100644 components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposable.kt create mode 100644 components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnknownErrorComposable.kt create mode 100644 components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnsupportedErrorComposable.kt create mode 100644 components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposable.kt create mode 100644 components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/InProgressComposable.kt delete mode 100644 components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposablePreview.kt delete mode 100644 components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposable.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b65cbd275..ba60605d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - [FIX] Fix empty response in faphub category - [FIX] New file manager uploading progress - [FIX] Fix build when no metrics enabled +- [FIX] Fix small issues with new file manager # 1.8.0 Attention: don't forget to add the flag for F-Droid before release diff --git a/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/AndroidShareHelper.kt b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/AndroidShareHelper.kt index 35506eb4c2..632a238333 100644 --- a/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/AndroidShareHelper.kt +++ b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/AndroidShareHelper.kt @@ -2,6 +2,7 @@ package com.flipperdevices.core.share import android.content.Context import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.ktx.jre.createClearNewFileWithMkDirs import com.squareup.anvil.annotations.ContributesBinding import okio.Path.Companion.toOkioPath import javax.inject.Inject @@ -12,8 +13,9 @@ class AndroidShareHelper @Inject constructor( ) : PlatformShareHelper { override fun provideSharableFile(fileName: String): PlatformSharableFile { - val path = SharableFile(context, fileName).toOkioPath() - return PlatformSharableFile(path) + val sharableFile = SharableFile(context, fileName) + sharableFile.createClearNewFileWithMkDirs() + return PlatformSharableFile(sharableFile.toOkioPath()) } override fun shareFile(file: PlatformSharableFile, title: String) { diff --git a/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/api/DownloadDecomposeComponent.kt b/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/api/DownloadDecomposeComponent.kt index 495869a718..ce803a9c59 100644 --- a/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/api/DownloadDecomposeComponent.kt +++ b/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/api/DownloadDecomposeComponent.kt @@ -1,9 +1,9 @@ package com.flipperdevices.filemanager.download.api import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.filemanager.download.model.DownloadableFile import com.flipperdevices.ui.decompose.ScreenDecomposeComponent import kotlinx.coroutines.flow.StateFlow -import okio.Path abstract class DownloadDecomposeComponent( componentContext: ComponentContext @@ -12,10 +12,7 @@ abstract class DownloadDecomposeComponent( abstract fun onCancel() - abstract fun download( - fullPath: Path, - size: Long - ) + abstract fun download(file: DownloadableFile) fun interface Factory { operator fun invoke( diff --git a/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/model/DownloadableFile.kt b/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/model/DownloadableFile.kt new file mode 100644 index 0000000000..b2125b540b --- /dev/null +++ b/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/model/DownloadableFile.kt @@ -0,0 +1,8 @@ +package com.flipperdevices.filemanager.download.model + +import okio.Path + +data class DownloadableFile( + val fullPath: Path, + val size: Long +) diff --git a/components/filemngr/download/impl/build.gradle.kts b/components/filemngr/download/impl/build.gradle.kts index 4b1450333f..489eafa83a 100644 --- a/components/filemngr/download/impl/build.gradle.kts +++ b/components/filemngr/download/impl/build.gradle.kts @@ -30,6 +30,7 @@ commonDependencies { implementation(projects.components.bridge.dao.api) implementation(projects.components.filemngr.download.api) + implementation(projects.components.filemngr.uiComponents) // Compose implementation(libs.compose.ui) diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt index 5f1cb33c1c..2002ad7bc9 100644 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt @@ -16,15 +16,14 @@ import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.filemanager.download.api.DownloadDecomposeComponent import com.flipperdevices.filemanager.download.impl.composable.DownloadingComposable -import com.flipperdevices.filemanager.download.impl.model.DownloadableFile import com.flipperdevices.filemanager.download.impl.viewmodel.DownloadViewModel +import com.flipperdevices.filemanager.download.model.DownloadableFile import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import me.gulya.anvil.assisted.ContributesAssistedFactory -import okio.Path import javax.inject.Provider @ContributesAssistedFactory(AppGraph::class, DownloadDecomposeComponent.Factory::class) @@ -42,15 +41,9 @@ class DownloadDecomposeComponentImpl @AssistedInject constructor( override fun onCancel() = downloadViewModel.onCancel() - override fun download( - fullPath: Path, - size: Long - ) = downloadViewModel.tryDownload( - file = DownloadableFile( - fullPath = fullPath, - size = size - ) - ) + override fun download(file: DownloadableFile) { + downloadViewModel.tryDownload(file = file) + } @Composable override fun Render() { diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/DownloadingComposable.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/DownloadingComposable.kt index 3bfe684da1..d2afa575bd 100644 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/DownloadingComposable.kt +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/DownloadingComposable.kt @@ -1,25 +1,14 @@ package com.flipperdevices.filemanager.download.impl.composable -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.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ui.ktx.clickableRipple -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.core.ktx.jre.toFormattedSize import com.flipperdevices.filemanager.download.impl.viewmodel.DownloadViewModel +import com.flipperdevices.filemanager.ui.components.transfer.FileTransferFullScreenComposable import flipperapp.components.filemngr.download.impl.generated.resources.fm_cancel import flipperapp.components.filemngr.download.impl.generated.resources.fm_downloading +import flipperapp.components.filemngr.download.impl.generated.resources.fm_in_progress_file_size +import flipperapp.components.filemngr.download.impl.generated.resources.fm_in_progress_speed import org.jetbrains.compose.resources.stringResource import flipperapp.components.filemngr.download.impl.generated.resources.Res as FDR @@ -29,38 +18,24 @@ fun DownloadingComposable( onCancel: () -> Unit, modifier: Modifier = Modifier ) { - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text( - text = stringResource(FDR.string.fm_downloading), - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(FDR.string.fm_cancel), - style = LocalTypography.current.bodyM14, - color = LocalPalletV2.current.action.danger.text.default, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - .clip(RoundedCornerShape(12.dp)) - .clickableRipple(onClick = onCancel), - textAlign = TextAlign.Center + FileTransferFullScreenComposable( + modifier = modifier, + title = stringResource(FDR.string.fm_downloading), + actionText = stringResource(FDR.string.fm_cancel), + onActionClick = onCancel, + progressTitle = state.fullPath.name, + progress = if (state.totalSize == 0L) 0f else state.downloadedSize / state.totalSize.toFloat(), + progressText = stringResource( + FDR.string.fm_in_progress_file_size, + state.downloadedSize.toFormattedSize(), + state.totalSize.toFormattedSize() + ), + speedText = when (state.downloadSpeed) { + 0L -> null + else -> stringResource( + FDR.string.fm_in_progress_speed, + state.downloadSpeed.toFormattedSize(), ) } - InProgressComposable( - state = state, - ) - } + ) } diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/InProgressComposable.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/InProgressComposable.kt deleted file mode 100644 index 3fcb6fcb24..0000000000 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/InProgressComposable.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.flipperdevices.filemanager.download.impl.composable - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ktx.jre.toFormattedSize -import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator -import com.flipperdevices.core.ui.theme.LocalPallet -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography -import com.flipperdevices.filemanager.download.impl.viewmodel.DownloadViewModel -import flipperapp.components.filemngr.download.impl.generated.resources.fm_downloading_file -import flipperapp.components.filemngr.download.impl.generated.resources.fm_in_progress_file_size -import flipperapp.components.filemngr.download.impl.generated.resources.fm_in_progress_speed -import org.jetbrains.compose.resources.stringResource -import flipperapp.components.filemngr.download.impl.generated.resources.Res as FDR - -@Composable -private fun InProgressDetailComposable( - state: DownloadViewModel.State.Downloading, - modifier: Modifier = Modifier -) { - Text( - text = stringResource( - FDR.string.fm_downloading_file, - state.fullPath.name, - state.downloadedSize.toFormattedSize(), - state.totalSize.toFormattedSize() - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) -} - -@Composable -private fun InProgressTitleComposable( - state: DownloadViewModel.State.Downloading, - modifier: Modifier = Modifier -) { - Text( - text = state.fullPath.name, - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) -} - -@Composable -internal fun InProgressComposable( - state: DownloadViewModel.State.Downloading, -) { - val animatedProgress by animateFloatAsState( - targetValue = if (state.totalSize == 0L) 0f else state.downloadedSize / state.totalSize.toFloat(), - animationSpec = tween(durationMillis = 500, easing = LinearEasing), - label = "Progress" - ) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - InProgressTitleComposable(state) - Spacer(Modifier.height(12.dp)) - FlipperProgressIndicator( - modifier = Modifier.padding(horizontal = 32.dp), - accentColor = LocalPalletV2.current.action.blue.border.primary.default, - secondColor = LocalPallet.current.actionOnFlipperProgress, - painter = null, - percent = animatedProgress - ) - Spacer(Modifier.height(8.dp)) - InProgressDetailComposable(state) - Text( - text = stringResource( - FDR.string.fm_in_progress_file_size, - state.downloadedSize.toFormattedSize(), - state.totalSize.toFormattedSize() - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource( - FDR.string.fm_in_progress_speed, - state.downloadSpeed.toFormattedSize(), - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } -} diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/model/DownloadableFile.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/model/DownloadableFile.kt deleted file mode 100644 index dbd48c624b..0000000000 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/model/DownloadableFile.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.flipperdevices.filemanager.download.impl.model - -import okio.Path - -class DownloadableFile( - val fullPath: Path, - val size: Long -) diff --git a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt index 61c2e0bb89..b0757a8733 100644 --- a/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt @@ -9,7 +9,7 @@ import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import com.flipperdevices.core.share.PlatformShareHelper import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel -import com.flipperdevices.filemanager.download.impl.model.DownloadableFile +import com.flipperdevices.filemanager.download.model.DownloadableFile import flipperapp.components.filemngr.download.impl.generated.resources.Res import flipperapp.components.filemngr.download.impl.generated.resources.fm_share_title import kotlinx.coroutines.Dispatchers @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -53,7 +52,6 @@ class DownloadViewModel @Inject constructor( pathOnFlipper = flipperFileFullPath.toString(), fileOnAndroid = pathOnAndroid.path, progressListener = { current, max -> - println("DownloadViewModel progressListener") _state.update { state -> (state as? State.Downloading) ?.copy(downloadedSize = current, totalSize = max) diff --git a/components/filemngr/editor/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/editor/composable/ContentPreview.kt b/components/filemngr/editor/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/editor/composable/ContentPreview.kt deleted file mode 100644 index bef9b9482f..0000000000 --- a/components/filemngr/editor/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/editor/composable/ContentPreview.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable - -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.flipperdevices.core.ui.theme.FlipperThemeInternal -import com.flipperdevices.filemanager.editor.composable.content.EditorLoadingContent -import com.flipperdevices.filemanager.editor.composable.content.ErrorContent -import com.flipperdevices.filemanager.editor.composable.content.TooBigContent - -@Preview -@Composable -private fun ErrorContentPreview() { - FlipperThemeInternal { - Scaffold { - ErrorContent(modifier = Modifier.padding(it)) - } - } -} - -@Preview -@Composable -private fun LoadingContentPreview() { - FlipperThemeInternal { - Scaffold { - EditorLoadingContent(modifier = Modifier.padding(it)) - } - } -} - -@Preview -@Composable -private fun TooBigContentPreview() { - FlipperThemeInternal { - Scaffold { - TooBigContent(modifier = Modifier.padding(it)) - } - } -} diff --git a/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml index 5aee40d618..de36895cc2 100644 --- a/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml +++ b/components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml @@ -4,7 +4,7 @@ Save File as... Save Downloading... - Uploading: %1$s [%2$s/%3$s] + Uploading... Speed: %1$s/s Cancel Allowed characters: %1$s @@ -13,4 +13,6 @@ Enter name: Save as New File Allowed Characters: %1$s + The file is too large! + Unfortunately, flipper Android doesn't allow to edit files larger: %1$s \ No newline at end of file diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt index ccc900748e..be9ebec916 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/FileDownloadDecomposeComponent.kt @@ -1,23 +1,26 @@ package com.flipperdevices.filemanager.editor.api -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.flipperdevices.core.ktx.jre.toFormattedSize import com.flipperdevices.filemanager.editor.composable.download.UploadingComposable import com.flipperdevices.filemanager.editor.viewmodel.DownloadViewModel +import com.flipperdevices.filemanager.ui.components.error.ErrorContentComposable +import com.flipperdevices.filemanager.ui.components.error.UnknownErrorComposable +import com.flipperdevices.filemanager.ui.components.error.UnsupportedErrorComposable +import com.flipperdevices.filemanager.util.constant.FileManagerConstants import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.ScreenDecomposeComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_error_too_large_desc +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_error_too_large_title import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_downloading import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn @@ -46,20 +49,18 @@ class FileDownloadDecomposeComponent @AssistedInject constructor( LaunchedEffect(downloadViewModel) { downloadViewModel.state .filterIsInstance() - .onEach { - onDownloaded.invoke() - }.launchIn(this) + .onEach { onDownloaded.invoke() } + .launchIn(this) } val state by downloadViewModel.state.collectAsState() when (val localState = state) { DownloadViewModel.State.CouldNotDownload -> { - Box(Modifier.fillMaxSize().background(Color.Red)) + UnknownErrorComposable() } - DownloadViewModel.State.Downloaded -> { - Box(Modifier.fillMaxSize().background(Color.Green)) - } + // Screen closed + DownloadViewModel.State.Downloaded -> Unit is DownloadViewModel.State.Downloading -> { UploadingComposable( @@ -75,11 +76,17 @@ class FileDownloadDecomposeComponent @AssistedInject constructor( } DownloadViewModel.State.TooLarge -> { - Box(Modifier.fillMaxSize().background(Color.Cyan)) + ErrorContentComposable( + text = stringResource(FME.string.fme_error_too_large_title), + desc = stringResource( + FME.string.fme_error_too_large_desc, + FileManagerConstants.LIMITED_SIZE_BYTES.toFormattedSize() + ) + ) } DownloadViewModel.State.Unsupported -> { - Box(Modifier.fillMaxSize().background(Color.Yellow)) + UnsupportedErrorComposable() } } } diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt index a5ab5c0e3d..4dc8a00e2a 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/api/UploadFileDecomposeComponent.kt @@ -1,20 +1,18 @@ package com.flipperdevices.filemanager.editor.api -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.instancekeeper.getOrCreate import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import com.flipperdevices.filemanager.editor.composable.download.UploadingComposable import com.flipperdevices.filemanager.editor.viewmodel.UploadFileViewModel +import com.flipperdevices.filemanager.ui.components.error.UnknownErrorComposable +import com.flipperdevices.filemanager.ui.components.error.UnsupportedErrorComposable import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.ScreenDecomposeComponent import dagger.assisted.Assisted @@ -69,15 +67,14 @@ class UploadFileDecomposeComponent @AssistedInject constructor( when (val localState = state) { UploadFileViewModel.State.Error -> { - Box(Modifier.fillMaxSize().background(Color.Red)) + UnsupportedErrorComposable() } - is UploadFileViewModel.State.Saved -> { - Box(Modifier.fillMaxSize().background(Color.Green)) - } + // The screen is closed + is UploadFileViewModel.State.Saved -> Unit UploadFileViewModel.State.Unsupported -> { - Box(Modifier.fillMaxSize().background(Color.Yellow)) + UnknownErrorComposable() } is UploadFileViewModel.State.Uploading -> { diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/EditorLoadingContent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/EditorLoadingContent.kt deleted file mode 100644 index 8f56c051c6..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/EditorLoadingContent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable.content - -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.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ui.ktx.placeholderConnecting -import kotlin.random.Random - -@Composable -fun EditorLoadingContent(modifier: Modifier = Modifier) { - val widths = remember(Unit) { - List( - size = 32, - init = { Random.nextDouble(from = 0.4, until = 0.9).toFloat() } - ) - } - Box(modifier.fillMaxSize()) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.Start - ) { - widths.forEach { width -> - Box( - modifier = Modifier.height(12.dp) - .fillMaxWidth(width) - .placeholderConnecting() - ) - } - } - } -} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/ErrorContent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/ErrorContent.kt deleted file mode 100644 index 1e5f6fbbed..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/ErrorContent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable.content - -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 ErrorContent(modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize()) { - // todo - Text("Unsopported") - } -} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/TooBigContent.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/TooBigContent.kt deleted file mode 100644 index 72b7e38e1d..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/content/TooBigContent.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable.content - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.flipperdevices.core.ui.theme.LocalPallet -import flipperapp.components.filemngr.editor.impl.generated.resources.fme_too_large_file -import org.jetbrains.compose.resources.stringResource -import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME - -@Composable -fun TooBigContent(modifier: Modifier = Modifier) { - Text( - modifier = modifier - .fillMaxWidth() - .background(LocalPallet.current.warningColor), - text = stringResource(FME.string.fme_too_large_file), - color = LocalPallet.current.textOnWarningBackground - ) -} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt deleted file mode 100644 index a81c80ab58..0000000000 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/InProgressComposable.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.flipperdevices.filemanager.editor.composable.download - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ktx.jre.toFormattedSize -import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator -import com.flipperdevices.core.ui.theme.LocalPallet -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography -import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_speed -import okio.Path -import org.jetbrains.compose.resources.stringResource -import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME - -@Composable -private fun InProgressTitleComposable( - fullPathOnFlipper: Path, - modifier: Modifier = Modifier -) { - Text( - text = fullPathOnFlipper.name, - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) -} - -@Composable -internal fun InProgressComposable( - progress: Float, - fullPathOnFlipper: Path, - downloaded: Long, - total: Long, - speed: Long, -) { - val animatedProgress by animateFloatAsState( - targetValue = progress, - animationSpec = tween(durationMillis = 500, easing = LinearEasing), - label = "Progress" - ) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - InProgressTitleComposable(fullPathOnFlipper) - Spacer(Modifier.height(12.dp)) - FlipperProgressIndicator( - modifier = Modifier.padding(horizontal = 32.dp), - accentColor = LocalPalletV2.current.action.blue.border.primary.default, - secondColor = LocalPallet.current.actionOnFlipperProgress, - painter = null, - percent = animatedProgress - ) - Spacer(Modifier.height(8.dp)) - if (total > 0) { - Text( - text = "${downloaded.toFormattedSize()}/${total.toFormattedSize()}", - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - Spacer(Modifier.height(8.dp)) - if (speed > 0) { - Text( - text = stringResource( - resource = FME.string.fme_status_speed, - speed.toFormattedSize() - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } -} diff --git a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt index 4f297d5c67..bbc2c4baa2 100644 --- a/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt +++ b/components/filemngr/editor/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/editor/composable/download/UploadingComposable.kt @@ -1,23 +1,11 @@ package com.flipperdevices.filemanager.editor.composable.download -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.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ui.ktx.clickableRipple -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.core.ktx.jre.toFormattedSize +import com.flipperdevices.filemanager.ui.components.transfer.FileTransferFullScreenComposable import flipperapp.components.filemngr.editor.impl.generated.resources.fme_cancel +import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_speed import okio.Path import org.jetbrains.compose.resources.stringResource import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME @@ -33,42 +21,21 @@ fun UploadingComposable( onCancel: () -> Unit, modifier: Modifier = Modifier ) { - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text( - text = title, - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(FME.string.fme_cancel), - style = LocalTypography.current.bodyM14, - color = LocalPalletV2.current.action.danger.text.default, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - .clip(RoundedCornerShape(12.dp)) - .clickableRipple(onClick = onCancel), - textAlign = TextAlign.Center + FileTransferFullScreenComposable( + modifier = modifier, + title = title, + actionText = stringResource(FME.string.fme_cancel), + onActionClick = onCancel, + progressText = fullPathOnFlipper.name, + progress = progress, + progressDetailText = if (max == 0L) null else "${current.toFormattedSize()}/${max.toFormattedSize()}", + progressTitle = fullPathOnFlipper.name, + speedText = when (speed) { + 0L -> null + else -> stringResource( + resource = FME.string.fme_status_speed, + speed.toFormattedSize() ) } - InProgressComposable( - progress = progress, - speed = speed, - fullPathOnFlipper = fullPathOnFlipper, - downloaded = current, - total = max - ) - } + ) } diff --git a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptionsPreview.kt b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptionsPreview.kt index 3985e31d3f..16beb0a1bd 100644 --- a/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptionsPreview.kt +++ b/components/filemngr/listing/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/BottomBarOptionsPreview.kt @@ -14,7 +14,10 @@ private fun BottomBarOptionsPreview() { onRename = {}, onCopyTo = {}, onMove = {}, - canRename = true + canRename = true, + canMove = true, + canDelete = true, + canExport = true ) } } 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 4f58d0750e..bac349b64f 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 @@ -16,6 +16,7 @@ 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.download.model.DownloadableFile import com.flipperdevices.filemanager.listing.api.FilesDecomposeComponent import com.flipperdevices.filemanager.listing.impl.composable.ComposableFileListScreen import com.flipperdevices.filemanager.listing.impl.composable.LaunchedEventsComposable @@ -132,6 +133,7 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( filesViewModel.onFilesChanged(listOf(listingItem)) } + @Suppress("LongMethod") @Composable override fun Render() { val multipleFilesPicker = uploadDecomposeComponent.rememberMultipleFilesPicker(path) @@ -146,7 +148,10 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( } LaunchedEventsComposable( deleteFilesViewModel = deleteFileViewModel, - onFileRemove = filesViewModel::fileDeleted, + onFileDelete = { path -> + selectionViewModel.deselect(path) + filesViewModel.fileDeleted(path) + }, ) ComposableFileListScreen( path = path, @@ -172,6 +177,14 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( }, onMove = { pathsWithType -> moveToCallback.invoke(pathsWithType.map(PathWithType::fullPath)) + }, + onExport = { pathsWithTypes -> + pathsWithTypes.firstOrNull()?.let { pathWithType -> + DownloadableFile( + fullPath = pathWithType.fullPath, + size = pathWithType.size + ) + }?.run(downloadDecomposeComponent::download) } ) FileOptionsBottomSheet( @@ -179,7 +192,14 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( slotNavigation = slotNavigation, selectionViewModel = selectionViewModel, deleteFileViewModel = deleteFileViewModel, - onDownloadFile = downloadDecomposeComponent::download, + onDownloadFile = { pathWithType -> + downloadDecomposeComponent.download( + file = DownloadableFile( + fullPath = pathWithType.fullPath, + size = pathWithType.size + ) + ) + }, onRename = { pathWithType -> renameDecomposeComponent.startRename(pathWithType.fullPath, pathWithType.fileType) }, 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 7e24aab687..aac4bccfdd 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -23,6 +24,8 @@ 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.ui.components.error.UnknownErrorComposable +import com.flipperdevices.filemanager.ui.components.error.UnsupportedErrorComposable import okio.Path @Suppress("LongMethod") @@ -44,6 +47,7 @@ fun ComposableFileListScreen( onCreate: (FileType) -> Unit, onRename: (PathWithType) -> Unit, onMove: (List) -> Unit, + onExport: (List) -> Unit, modifier: Modifier = Modifier ) { val canDeleteFiles by deleteFileViewModel.canDeleteFiles.collectAsState() @@ -112,10 +116,16 @@ fun ComposableFileListScreen( } FilesViewModel.State.Unsupported -> { - item { NoListingFeatureComposable() } + item(span = { GridItemSpan(maxLineSpan) }) { + UnsupportedErrorComposable() + } } - FilesViewModel.State.CouldNotListPath -> Unit + FilesViewModel.State.CouldNotListPath -> { + item(span = { GridItemSpan(maxLineSpan) }) { + UnknownErrorComposable() + } + } } } FilesFailComposable( @@ -131,7 +141,8 @@ fun ComposableFileListScreen( selectionViewModel = selectionViewModel, deleteFileViewModel = deleteFileViewModel, onRename = onRename, - onMove = onMove + onMove = onMove, + onExport = onExport ) } } 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 84325abfb5..d3ee334515 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 @@ -10,13 +10,13 @@ import okio.Path @Composable fun LaunchedEventsComposable( deleteFilesViewModel: DeleteFilesViewModel, - onFileRemove: (Path) -> Unit + onFileDelete: (Path) -> Unit ) { LaunchedEffect(deleteFilesViewModel) { deleteFilesViewModel.event.onEach { when (it) { DeleteFilesViewModel.Event.CouldNotDeleteSomeFiles -> Unit - is DeleteFilesViewModel.Event.FileDeleted -> onFileRemove.invoke(it.path) + is DeleteFilesViewModel.Event.FileDeleted -> onFileDelete.invoke(it.path) } }.launchIn(this) } 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 cbd8438b34..95d03667c2 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 @@ -10,7 +10,6 @@ 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.SelectionViewModel -import okio.Path @Composable fun FileOptionsBottomSheet( @@ -18,7 +17,7 @@ fun FileOptionsBottomSheet( slotNavigation: SlotNavigation, selectionViewModel: SelectionViewModel, deleteFileViewModel: DeleteFilesViewModel, - onDownloadFile: (Path, Long) -> Unit, + onDownloadFile: (PathWithType) -> Unit, onRename: (PathWithType) -> Unit, onMoveTo: (PathWithType) -> Unit, modifier: Modifier = Modifier @@ -41,7 +40,7 @@ fun FileOptionsBottomSheet( slotNavigation.dismiss() }, onExport = { - onDownloadFile.invoke(pathWithType.fullPath, pathWithType.size) + onDownloadFile.invoke(pathWithType) slotNavigation.dismiss() }, onDelete = { @@ -50,6 +49,7 @@ fun FileOptionsBottomSheet( }, onMoveTo = { onMoveTo.invoke(pathWithType) + slotNavigation.dismiss() } ) } 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 02669be661..7f0a3606fe 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 @@ -42,6 +42,7 @@ import flipperapp.components.filemngr.ui_components.generated.resources.ic_more_ import flipperapp.components.filemngr.ui_components.generated.resources.ic_move import flipperapp.components.filemngr.ui_components.generated.resources.ic_trash_white import flipperapp.components.filemngr.ui_components.generated.resources.ic_upload +import kotlinx.collections.immutable.toImmutableList import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import flipperapp.components.filemngr.listing.impl.generated.resources.Res as FML @@ -85,6 +86,9 @@ private fun MoreBottomBarOptions( @Composable fun BottomBarOptions( canRename: Boolean, + canDelete: Boolean, + canMove: Boolean, + canExport: Boolean, onRename: () -> Unit, onExport: () -> Unit, onMove: () -> Unit, @@ -104,27 +108,32 @@ fun BottomBarOptions( .background(LocalPalletV2.current.surface.border.default.secondary) .padding(1.dp) .background(LocalPalletV2.current.surface.popUp.body.default), - horizontalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically ) { VerticalTextIconButton( text = stringResource(FML.string.fml_dialog_delete_btn), painter = painterResource(FR.drawable.ic_trash_white), iconTint = LocalPalletV2.current.action.danger.icon.default, + iconDisabledTint = LocalPalletV2.current.action.danger.icon.disabled, textColor = LocalPalletV2.current.action.danger.text.default, - onClick = onDelete + textDisabledColor = LocalPalletV2.current.action.danger.text.disabled, + onClick = onDelete, + isEnabled = canDelete ) VerticalTextIconButton( text = stringResource(FML.string.fml_move), painter = painterResource(FR.drawable.ic_move), onClick = onMove, + isEnabled = canMove ) VerticalTextIconButton( text = stringResource(FML.string.fml_export), painter = painterResource(FR.drawable.ic_upload), - onClick = onExport + onClick = onExport, + isEnabled = canExport ) MoreBottomBarOptions( onCopyTo = onCopyTo, @@ -142,6 +151,7 @@ fun FullScreenBottomBarOptions( selectionState: SelectionViewModel.State, onRename: (PathWithType) -> Unit, onMove: (List) -> Unit, + onExport: (List) -> Unit, modifier: Modifier = Modifier ) { Box( @@ -157,6 +167,9 @@ fun FullScreenBottomBarOptions( ) { BottomBarOptions( canRename = selectionState.canRename, + canMove = selectionState.canMove, + canDelete = selectionState.canDelete, + canExport = selectionState.canExport, onMove = { onMove.invoke(selectionState.selected.toList()) }, @@ -170,7 +183,9 @@ fun FullScreenBottomBarOptions( deleteFileViewModel.tryDelete(selectionState.selected.map(PathWithType::fullPath)) selectionViewModel.toggleMode() }, - onExport = {}, // todo + onExport = { + onExport.invoke(selectionState.selected.toImmutableList()) + }, onCopyTo = {} // todo ) } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/VerticalTextIconButton.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/VerticalTextIconButton.kt index d6ccd4bd42..ce4bc887ec 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/VerticalTextIconButton.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/options/VerticalTextIconButton.kt @@ -1,5 +1,6 @@ package com.flipperdevices.filemanager.listing.impl.composable.options +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -33,21 +34,21 @@ fun VerticalTextIconButton( Column( modifier = modifier .clip(RoundedCornerShape(12.dp)) - .clickableRipple(onClick = onClick) - .padding(vertical = 12.dp), + .clickableRipple(onClick = { if (isEnabled) onClick.invoke() }) + .padding(vertical = 12.dp, horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(2.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( painter = painter, - tint = if (isEnabled) iconTint else iconDisabledTint, + tint = animateColorAsState(if (isEnabled) iconTint else iconDisabledTint).value, modifier = Modifier.size(24.dp), contentDescription = null ) Text( text = text, style = LocalTypography.current.subtitleM12, - color = if (isEnabled) textColor else textDisabledColor + color = animateColorAsState(if (isEnabled) textColor else textDisabledColor).value ) } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/SelectionViewModel.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/SelectionViewModel.kt index 45a88096c6..0ebddaac10 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/SelectionViewModel.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/viewmodel/SelectionViewModel.kt @@ -1,5 +1,6 @@ package com.flipperdevices.filemanager.listing.impl.viewmodel +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import com.flipperdevices.filemanager.listing.impl.model.PathWithType import kotlinx.collections.immutable.ImmutableSet @@ -7,7 +8,10 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import okio.Path import javax.inject.Inject class SelectionViewModel @Inject constructor() : DecomposeViewModel() { @@ -28,6 +32,15 @@ class SelectionViewModel @Inject constructor() : DecomposeViewModel() { } } + fun deselect(path: Path) { + viewModelScope.launch { + state.first() + .selected + .firstOrNull { it.fullPath.name == path.name } + ?.run(::deselect) + } + } + fun select(path: PathWithType) { select(listOf(path)) } @@ -59,5 +72,8 @@ class SelectionViewModel @Inject constructor() : DecomposeViewModel() { val isEnabled: Boolean = false ) { val canRename: Boolean = selected.size == 1 + val canExport: Boolean = selected.size == 1 && selected.all { it.fileType == FileType.FILE } + val canMove: Boolean = selected.size >= 1 + val canDelete: Boolean = selected.size >= 1 } } diff --git a/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt b/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt index 05dfe967f3..c1ababdc3d 100644 --- a/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt +++ b/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/ComposableFilesSearchScreen.kt @@ -15,6 +15,7 @@ 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 com.flipperdevices.filemanager.ui.components.error.UnsupportedErrorComposable import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterIsInstance import okio.Path @@ -58,7 +59,7 @@ fun ComposableFilesSearchScreen( } SearchViewModel.State.Unsupported -> { - NoListingFeatureComposable() + UnsupportedErrorComposable() } } } diff --git a/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt b/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt deleted file mode 100644 index 7b365ddd20..0000000000 --- a/components/filemngr/search/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/search/impl/composable/NoListingFeatureComposable.kt +++ /dev/null @@ -1,15 +0,0 @@ -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/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt index 1b72819e70..9f2f54bb87 100644 --- a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt +++ b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/ComposableTransferScreen.kt @@ -1,6 +1,5 @@ package com.flipperdevices.filemanager.transfer.impl.composable -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -14,12 +13,13 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.flipperdevices.core.preference.pb.FileManagerOrientation import com.flipperdevices.filemanager.transfer.api.TransferDecomposeComponent.Param import com.flipperdevices.filemanager.transfer.impl.viewmodel.FilesViewModel import com.flipperdevices.filemanager.transfer.impl.viewmodel.OptionsViewModel +import com.flipperdevices.filemanager.ui.components.error.UnknownErrorComposable +import com.flipperdevices.filemanager.ui.components.error.UnsupportedErrorComposable import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardPlaceholderComposable import okio.Path @@ -73,7 +73,7 @@ fun ComposableTransferScreen( FilesViewModel.State.CouldNotListPath -> { item( span = { GridItemSpan(maxLineSpan) }, - content = { Box(Modifier.fillMaxSize().background(Color.Red)) } + content = { UnknownErrorComposable() } ) } @@ -107,7 +107,7 @@ fun ComposableTransferScreen( FilesViewModel.State.Unsupported -> { item( span = { GridItemSpan(maxLineSpan) }, - content = { UnsupportedComposable() } + content = { UnsupportedErrorComposable() } ) } } diff --git a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt b/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt deleted file mode 100644 index 09cf387bce..0000000000 --- a/components/filemngr/transfer/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/transfer/impl/composable/UnsupportedComposable.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.flipperdevices.filemanager.transfer.impl.composable - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color - -@Composable -fun UnsupportedComposable(modifier: Modifier = Modifier) { - Box(modifier.fillMaxSize().background(Color.Magenta)) -} diff --git a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposablePreview.kt b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposablePreview.kt new file mode 100644 index 0000000000..d67a963f1d --- /dev/null +++ b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposablePreview.kt @@ -0,0 +1,18 @@ +package com.flipperdevices.filemanager.ui.components.error + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.theme.FlipperThemeInternal + +@Preview +@Composable +@Suppress("MaximumLineLength", "MaxLineLength") +private fun ErrorContentComposablePreview() { + FlipperThemeInternal { + ErrorContentComposable( + text = "Some unknown error happened :(", + desc = "Sorry, but we don't know why that happened so it's impossible to resolve. Try to turn off and turn on the device! Good luck!", + onRetry = {} + ) + } +} diff --git a/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposablePreview.kt b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposablePreview.kt new file mode 100644 index 0000000000..9e25aa3e64 --- /dev/null +++ b/components/filemngr/ui-components/src/androidMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposablePreview.kt @@ -0,0 +1,22 @@ +package com.flipperdevices.filemanager.ui.components.transfer + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.theme.FlipperThemeInternal + +@Preview +@Composable +private fun FileTransferFullScreenComposablePreview() { + FlipperThemeInternal { + FileTransferFullScreenComposable( + title = "Downloading", + actionText = "Cancel", + onActionClick = {}, + progressTitle = "flipperfile.txt", + progress = 0.3f, + progressText = "Downloading 3KiB/5KiB", + speedText = "Speed: 10KiB/s", + progressDetailText = "1/3 items finished" + ) + } +} diff --git a/components/filemngr/ui-components/src/commonMain/composeResources/values/strings.xml b/components/filemngr/ui-components/src/commonMain/composeResources/values/strings.xml index e9e0a1d0bf..79340ecbc6 100644 --- a/components/filemngr/ui-components/src/commonMain/composeResources/values/strings.xml +++ b/components/filemngr/ui-components/src/commonMain/composeResources/values/strings.xml @@ -5,4 +5,11 @@ Insert a microSD card in Flipper Zero to access Files Used Total + Retry + + Unsupported Feature + Unfortunately, this feature is no supported on current device + + Unexpected error + Unexpected error has occur \ No newline at end of file diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposable.kt new file mode 100644 index 0000000000..2578b33d34 --- /dev/null +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/ErrorContentComposable.kt @@ -0,0 +1,76 @@ +package com.flipperdevices.filemanager.ui.components.error + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.painter.Painter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalTypography +import flipperapp.components.filemngr.ui_components.generated.resources.Res +import flipperapp.components.filemngr.ui_components.generated.resources.filemngr_error_retry +import flipperapp.components.filemngr.ui_components.generated.resources.ic__no_files_black +import flipperapp.components.filemngr.ui_components.generated.resources.ic__no_files_white +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +fun ErrorContentComposable( + text: String, + desc: String, + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null, + painter: Painter = painterResource( + when { + MaterialTheme.colors.isLight -> Res.drawable.ic__no_files_white + else -> Res.drawable.ic__no_files_black + } + ), +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) + ) { + Image( + modifier = Modifier.size(124.dp), + painter = painter, + contentDescription = null + ) + Text( + text = text, + style = LocalTypography.current.bodyM14, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = desc, + textAlign = TextAlign.Center, + style = LocalTypography.current.bodyR14.copy( + color = LocalPallet.current.text30 + ) + ) + if (onRetry != null) { + Text( + modifier = Modifier + .padding(top = 6.dp) + .clickableRipple(onClick = onRetry), + text = stringResource(Res.string.filemngr_error_retry), + textAlign = TextAlign.Center, + style = LocalTypography.current.buttonM16.copy( + color = LocalPallet.current.accentSecond + ) + ) + } + } +} diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnknownErrorComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnknownErrorComposable.kt new file mode 100644 index 0000000000..444b1a4c09 --- /dev/null +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnknownErrorComposable.kt @@ -0,0 +1,21 @@ +package com.flipperdevices.filemanager.ui.components.error + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import flipperapp.components.filemngr.ui_components.generated.resources.Res +import flipperapp.components.filemngr.ui_components.generated.resources.filemngr_error_unknown_desc +import flipperapp.components.filemngr.ui_components.generated.resources.filemngr_error_unknown_title +import org.jetbrains.compose.resources.stringResource + +@Composable +fun UnknownErrorComposable( + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null, +) { + ErrorContentComposable( + modifier = modifier, + onRetry = onRetry, + text = stringResource(Res.string.filemngr_error_unknown_title), + desc = stringResource(Res.string.filemngr_error_unknown_desc) + ) +} diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnsupportedErrorComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnsupportedErrorComposable.kt new file mode 100644 index 0000000000..69405ea67b --- /dev/null +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/error/UnsupportedErrorComposable.kt @@ -0,0 +1,21 @@ +package com.flipperdevices.filemanager.ui.components.error + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import flipperapp.components.filemngr.ui_components.generated.resources.filemngr_error_unsupported_desc +import flipperapp.components.filemngr.ui_components.generated.resources.filemngr_error_unsupported_title +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.ui_components.generated.resources.Res as FR + +@Composable +fun UnsupportedErrorComposable( + modifier: Modifier = Modifier, + onRetry: (() -> Unit)? = null, +) { + ErrorContentComposable( + modifier = modifier, + onRetry = onRetry, + text = stringResource(FR.string.filemngr_error_unsupported_title), + desc = stringResource(FR.string.filemngr_error_unsupported_desc) + ) +} diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposable.kt new file mode 100644 index 0000000000..ebcc008ff3 --- /dev/null +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/FileTransferFullScreenComposable.kt @@ -0,0 +1,70 @@ +package com.flipperdevices.filemanager.ui.components.transfer + +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.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.clickableRipple +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography + +@Composable +fun FileTransferFullScreenComposable( + title: String, + actionText: String, + onActionClick: () -> Unit, + progressTitle: String, + progress: Float, + modifier: Modifier = Modifier, + progressText: String? = null, + progressDetailText: String? = null, + speedText: String? = null +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = title, + style = LocalTypography.current.titleB18, + color = LocalPalletV2.current.text.title.primary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + + Text( + text = actionText, + style = LocalTypography.current.bodyM14, + color = LocalPalletV2.current.action.danger.text.default, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickableRipple(onClick = onActionClick) + .padding(12.dp), + textAlign = TextAlign.Center + ) + } + FileTransferProgressComposable( + progressTitle = progressTitle, + progressDetailText = progressDetailText, + progress = progress, + progressText = progressText, + speedText = speedText + ) + } +} diff --git a/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/InProgressComposable.kt b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/InProgressComposable.kt new file mode 100644 index 0000000000..ab4cb765e6 --- /dev/null +++ b/components/filemngr/ui-components/src/commonMain/kotlin/com/flipperdevices/filemanager/ui/components/transfer/InProgressComposable.kt @@ -0,0 +1,107 @@ +package com.flipperdevices.filemanager.ui.components.transfer + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalPalletV2 +import com.flipperdevices.core.ui.theme.LocalTypography + +@Suppress("LongMethod") +@Composable +internal fun FileTransferProgressComposable( + progressTitle: String, + progress: Float, + modifier: Modifier = Modifier, + progressText: String? = null, + progressDetailText: String? = null, + speedText: String? = null +) { + val animatedProgress by animateFloatAsState( + targetValue = progress.coerceIn(0f, 1f), + animationSpec = tween(durationMillis = 500, easing = LinearEasing), + label = "Progress" + ) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + ) { + Text( + text = progressTitle, + style = LocalTypography.current.titleB18, + color = LocalPalletV2.current.text.title.primary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(12.dp)) + FlipperProgressIndicator( + modifier = Modifier.padding(horizontal = 32.dp), + accentColor = LocalPalletV2.current.action.blue.border.primary.default, + secondColor = LocalPallet.current.actionOnFlipperProgress, + painter = null, + percent = animatedProgress + ) + Spacer(Modifier.height(8.dp)) + AnimatedVisibility(progressDetailText != null) { + if (progressDetailText != null) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = progressDetailText, + style = LocalTypography.current.subtitleM12, + color = LocalPalletV2.current.text.body.secondary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + } + } + } + AnimatedVisibility(progressText != null) { + if (progressText != null) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = progressText, + style = LocalTypography.current.subtitleM12, + color = LocalPalletV2.current.text.body.secondary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + } + } + } + AnimatedVisibility(speedText != null) { + if (speedText != null) { + Text( + text = speedText, + style = LocalTypography.current.subtitleM12, + color = LocalPalletV2.current.text.body.secondary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + } +} diff --git a/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposablePreview.kt b/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposablePreview.kt deleted file mode 100644 index 3b4f35f766..0000000000 --- a/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposablePreview.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.flipperdevices.filemanager.upload.impl.composable - -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import com.flipperdevices.core.ui.theme.FlipperThemeInternal -import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent - -@Preview -@Composable -private fun InProgressComposablePreview() { - FlipperThemeInternal { - InProgressComposable( - state = UploaderDecomposeComponent.State.Uploading( - currentItemIndex = 0, - totalItemsAmount = 3, - uploadedSize = 123456, - totalSize = 223456, - currentItem = UploaderDecomposeComponent.UploadingItem( - fileName = "file_name.txt", - uploadedSize = 1234L, - totalSize = 1234567L - ) - ), - speed = 1222L - ) - } -} diff --git a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposable.kt b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposable.kt deleted file mode 100644 index f1009f6cb3..0000000000 --- a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/InProgressComposable.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.flipperdevices.filemanager.upload.impl.composable - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ktx.jre.toFormattedSize -import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator -import com.flipperdevices.core.ui.theme.LocalPallet -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography -import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent -import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_file_size -import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_items -import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_speed -import flipperapp.components.filemngr.upload.impl.generated.resources.fm_uploading_file -import org.jetbrains.compose.resources.stringResource -import flipperapp.components.filemngr.upload.impl.generated.resources.Res as FUR - -@Composable -private fun InProgressDetailComposable( - state: UploaderDecomposeComponent.State.Uploading, - modifier: Modifier = Modifier -) { - if (state.totalItemsAmount > 1) { - Text( - text = stringResource( - FUR.string.fm_uploading_file, - state.currentItem.fileName, - state.currentItem.uploadedSize.toFormattedSize(), - state.currentItem.totalSize.toFormattedSize() - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun InProgressTitleComposable( - state: UploaderDecomposeComponent.State.Uploading, - modifier: Modifier = Modifier -) { - Text( - text = when { - state.totalItemsAmount == 1 -> state.currentItem.fileName - else -> stringResource( - FUR.string.fm_in_progress_items, - state.currentItemIndex.plus(1), - state.totalItemsAmount - ) - }, - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) -} - -@Composable -internal fun InProgressComposable( - state: UploaderDecomposeComponent.State.Uploading, - speed: Long?, -) { - val animatedProgress by animateFloatAsState( - targetValue = if (state.totalSize == 0L) 0f else state.uploadedSize / state.totalSize.toFloat(), - animationSpec = tween(durationMillis = 500, easing = LinearEasing), - label = "Progress" - ) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - InProgressTitleComposable(state) - Spacer(Modifier.height(12.dp)) - FlipperProgressIndicator( - modifier = Modifier.padding(horizontal = 32.dp), - accentColor = LocalPalletV2.current.action.blue.border.primary.default, - secondColor = LocalPallet.current.actionOnFlipperProgress, - painter = null, - percent = animatedProgress - ) - Spacer(Modifier.height(8.dp)) - InProgressDetailComposable(state) - Text( - text = stringResource( - FUR.string.fm_in_progress_file_size, - state.uploadedSize.toFormattedSize(), - state.totalSize.toFormattedSize() - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(8.dp)) - speed?.let { - Text( - text = stringResource( - FUR.string.fm_in_progress_speed, - speed.toFormattedSize(), - ), - style = LocalTypography.current.subtitleM12, - color = LocalPalletV2.current.text.body.secondary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } -} diff --git a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/UploadingComposable.kt b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/UploadingComposable.kt index c0c0606952..713d729a48 100644 --- a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/UploadingComposable.kt +++ b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/UploadingComposable.kt @@ -1,25 +1,16 @@ package com.flipperdevices.filemanager.upload.impl.composable -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.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.flipperdevices.core.ui.ktx.clickableRipple -import com.flipperdevices.core.ui.theme.LocalPalletV2 -import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.core.ktx.jre.toFormattedSize +import com.flipperdevices.filemanager.ui.components.transfer.FileTransferFullScreenComposable import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent import flipperapp.components.filemngr.upload.impl.generated.resources.fm_cancel +import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_file_size +import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_items +import flipperapp.components.filemngr.upload.impl.generated.resources.fm_in_progress_speed import flipperapp.components.filemngr.upload.impl.generated.resources.fm_uploading +import flipperapp.components.filemngr.upload.impl.generated.resources.fm_uploading_file import org.jetbrains.compose.resources.stringResource import flipperapp.components.filemngr.upload.impl.generated.resources.Res as FUR @@ -30,39 +21,39 @@ fun UploadingComposable( onCancel: () -> Unit, modifier: Modifier = Modifier ) { - Box( - modifier = modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text( - text = stringResource(FUR.string.fm_uploading), - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center + FileTransferFullScreenComposable( + modifier = modifier, + title = stringResource(FUR.string.fm_uploading), + actionText = stringResource(FUR.string.fm_cancel), + onActionClick = onCancel, + progress = if (state.totalSize == 0L) 0f else state.uploadedSize / state.totalSize.toFloat(), + progressDetailText = when (state.totalItemsAmount) { + 1 -> null + else -> stringResource( + FUR.string.fm_uploading_file, + state.currentItem.fileName, + state.currentItem.uploadedSize.toFormattedSize(), + state.currentItem.totalSize.toFormattedSize() ) - - Text( - text = stringResource(FUR.string.fm_cancel), - style = LocalTypography.current.bodyM14, - color = LocalPalletV2.current.action.danger.text.default, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - .clip(RoundedCornerShape(12.dp)) - .clickableRipple(onClick = onCancel), - textAlign = TextAlign.Center + }, + progressTitle = when { + state.totalItemsAmount == 1 -> state.currentItem.fileName + else -> stringResource( + FUR.string.fm_in_progress_items, + state.currentItemIndex.plus(1), + state.totalItemsAmount + ) + }, + progressText = stringResource( + FUR.string.fm_in_progress_file_size, + state.uploadedSize.toFormattedSize(), + state.totalSize.toFormattedSize() + ), + speedText = speed?.let { _ -> + stringResource( + FUR.string.fm_in_progress_speed, + speed.toFormattedSize(), ) - } - InProgressComposable( - state = state, - speed = speed - ) - } + }, + ) } diff --git a/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt index 2efa4a8d6d..017237d0de 100644 --- a/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt +++ b/components/filemngr/util/src/commonMain/kotlin/com/flipperdevices/filemanager/util/serialization/PathSerializer.kt @@ -1,7 +1,6 @@ package com.flipperdevices.filemanager.util.serialization import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor