diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e91f3dc49..f075036de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 1.8.1 - In Progress +- [FIX] New file manager uploading progress - [FIX] Fix build when no metrics enabled # 1.8.0 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 index 837ebd8e41..67b7067d1c 100644 --- 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 @@ -27,7 +27,7 @@ fun UploaderDecomposeComponent.RenderLoadingScreen(modifier: Modifier = Modifier Render( state = uploaderState, speedState = speedState, - onCancel = ::onCancel, + onCancelClick = ::onCancel, modifier = Modifier .fillMaxSize() .background(LocalPalletV2.current.surface.backgroundMain.body) 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 d2327eed05..d4aa460065 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 @@ -17,7 +17,6 @@ abstract class FilesDecomposeComponent( pathChangedCallback: PathChangedCallback, fileSelectedCallback: FileSelectedCallback, searchCallback: SearchCallback, - uploadCallback: UploadCallback ): FilesDecomposeComponent } diff --git a/components/filemngr/listing/impl/build.gradle.kts b/components/filemngr/listing/impl/build.gradle.kts index ec1d55fcea..50b95cd873 100644 --- a/components/filemngr/listing/impl/build.gradle.kts +++ b/components/filemngr/listing/impl/build.gradle.kts @@ -31,6 +31,7 @@ commonDependencies { implementation(projects.components.filemngr.uiComponents) implementation(projects.components.filemngr.listing.api) implementation(projects.components.filemngr.main.api) + implementation(projects.components.filemngr.upload.api) // 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 10a79bd6db..b322b458f6 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 @@ -2,6 +2,7 @@ package com.flipperdevices.filemanager.listing.impl.api import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.childContext import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -22,6 +23,7 @@ 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.upload.api.UploadDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -38,13 +40,13 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( @Assisted private val pathChangedCallback: PathChangedCallback, @Assisted private val fileSelectedCallback: FileSelectedCallback, @Assisted private val searchCallback: SearchCallback, - @Assisted private val uploadCallback: UploadCallback, private val storageInfoViewModelFactory: Provider, private val optionsInfoViewModelFactory: Provider, private val editFileViewModelFactory: Provider, private val deleteFilesViewModelFactory: Provider, private val filesViewModelFactory: FilesViewModel.Factory, - private val createSelectionViewModel: Provider + private val createSelectionViewModel: Provider, + private val uploadDecomposeComponentFactory: UploadDecomposeComponent.Factory, ) : FilesDecomposeComponent(componentContext) { private val slotNavigation = SlotNavigation() @@ -55,10 +57,21 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( childFactory = { bottomSheetFile, _ -> bottomSheetFile } ) - private val selectionViewModel = instanceKeeper.getOrCreate(path.toString()) { + private val selectionViewModel = instanceKeeper.getOrCreate("selectionViewModel_$path") { createSelectionViewModel.get() } + val filesViewModel = instanceKeeper.getOrCreate("filesViewModel_$path") { + filesViewModelFactory.invoke(path) + } + + private val uploadDecomposeComponent by lazy { + uploadDecomposeComponentFactory.invoke( + componentContext = childContext("FilesDecomposeComponent_uploadDecomposeComponent"), + onFilesChanged = filesViewModel::onFilesChanged, + ) + } + private val backCallback = BackCallback { val parent = path.parent when { @@ -86,9 +99,7 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( @Composable override fun Render() { - val filesViewModel = viewModelWithFactory(path.toString()) { - filesViewModelFactory.invoke(path) - } + val multipleFilesPicker = uploadDecomposeComponent.rememberMultipleFilesPicker(path) val storageInfoViewModel = viewModelWithFactory(path.root.toString()) { storageInfoViewModelFactory.get() } @@ -116,7 +127,7 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( storageInfoViewModel = storageInfoViewModel, selectionViewModel = selectionViewModel, onBack = onBack::invoke, - onUploadClick = uploadCallback::invoke, + onUploadClick = multipleFilesPicker::startFilePicker, onPathChange = pathChangedCallback::invoke, onFileMoreClick = slotNavigation::activate, onSearchClick = searchCallback::invoke, @@ -129,5 +140,6 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( createFileViewModel = createFileViewModel, deleteFileViewModel = deleteFileViewModel ) + uploadDecomposeComponent.Render() } } 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 5750704e55..6e908971a1 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 @@ -115,6 +115,19 @@ class FilesViewModel @AssistedInject constructor( } } + fun onFilesChanged(items: List) { + _state.update { state -> + (state as? State.Loaded)?.let { loadedState -> + val newItemsNames = items.map(ListingItem::fileName) + val newFiles = loadedState.files + .filter { item -> !newItemsNames.contains(item.fileName) } + .plus(items) + .toImmutableList() + loadedState.copy(files = newFiles) + } ?: state + } + } + private suspend fun invalidate( featureStatus: FFeatureStatus ) = withLock(mutex, "invalidate") { 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 9005acfdf7..64345e4ffa 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,7 +12,6 @@ import com.flipperdevices.filemanager.listing.api.FilesDecomposeComponent import com.flipperdevices.filemanager.main.api.FileManagerDecomposeComponent import com.flipperdevices.filemanager.main.impl.model.FileManagerNavigationConfig import com.flipperdevices.filemanager.search.api.SearchDecomposeComponent -import com.flipperdevices.filemanager.upload.api.UploadDecomposeComponent import com.flipperdevices.ui.decompose.DecomposeComponent import com.flipperdevices.ui.decompose.DecomposeOnBackParameter import com.flipperdevices.ui.decompose.popOr @@ -26,7 +25,6 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, @Assisted private val onBack: DecomposeOnBackParameter, private val filesDecomposeComponentFactory: FilesDecomposeComponent.Factory, - private val uploadDecomposeComponentFactory: UploadDecomposeComponent.Factory, private val searchDecomposeComponentFactory: SearchDecomposeComponent.Factory, private val editorDecomposeComponentFactory: FileManagerEditorDecomposeComponent.Factory, ) : FileManagerDecomposeComponent(), @@ -55,19 +53,10 @@ class FileManagerDecomposeComponentImpl @AssistedInject constructor( fileSelectedCallback = { navigation.pushNew(FileManagerNavigationConfig.Edit(it)) }, - uploadCallback = { navigation.pushNew(FileManagerNavigationConfig.Upload(config.path)) }, searchCallback = { navigation.pushNew(FileManagerNavigationConfig.Search(config.path)) }, ) } - is FileManagerNavigationConfig.Upload -> { - uploadDecomposeComponentFactory.invoke( - componentContext = componentContext, - path = config.path, - onFinish = navigation::pop - ) - } - is FileManagerNavigationConfig.Search -> { searchDecomposeComponentFactory.invoke( componentContext = componentContext, 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 dceeaefe40..d98b19bc90 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 @@ -15,12 +15,6 @@ sealed interface FileManagerNavigationConfig { val path: Path ) : FileManagerNavigationConfig - @Serializable - data class Upload( - @Serializable(with = PathSerializer::class) - val path: Path - ) : FileManagerNavigationConfig - @Serializable data class Edit( @Serializable(with = PathSerializer::class) diff --git a/components/filemngr/upload/api/build.gradle.kts b/components/filemngr/upload/api/build.gradle.kts index d1d1bee1ca..a84819b7d0 100644 --- a/components/filemngr/upload/api/build.gradle.kts +++ b/components/filemngr/upload/api/build.gradle.kts @@ -10,6 +10,8 @@ commonDependencies { implementation(projects.components.core.ui.decompose) implementation(projects.components.deeplink.api) + implementation(projects.components.bridge.connection.feature.storage.api) + implementation(libs.compose.ui) implementation(libs.decompose) diff --git a/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/MultipleFilesPicker.kt b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/MultipleFilesPicker.kt new file mode 100644 index 0000000000..983744d631 --- /dev/null +++ b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/MultipleFilesPicker.kt @@ -0,0 +1,5 @@ +package com.flipperdevices.filemanager.upload.api + +fun interface MultipleFilesPicker { + fun startFilePicker() +} diff --git a/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploadDecomposeComponent.kt b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploadDecomposeComponent.kt index b2d474e376..04620cded1 100644 --- a/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploadDecomposeComponent.kt +++ b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploadDecomposeComponent.kt @@ -1,17 +1,22 @@ package com.flipperdevices.filemanager.upload.api +import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext -import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import okio.Path -abstract class UploadDecomposeComponent( - componentContext: ComponentContext -) : ScreenDecomposeComponent(componentContext) { +interface UploadDecomposeComponent { + + @Composable + fun rememberMultipleFilesPicker(path: Path): MultipleFilesPicker + + @Composable + fun Render() + fun interface Factory { operator fun invoke( componentContext: ComponentContext, - path: Path, - onFinish: () -> Unit + onFilesChanged: (List) -> Unit, ): UploadDecomposeComponent } } diff --git a/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploaderDecomposeComponent.kt b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploaderDecomposeComponent.kt index a90236691b..812bbfc49b 100644 --- a/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploaderDecomposeComponent.kt +++ b/components/filemngr/upload/api/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/api/UploaderDecomposeComponent.kt @@ -3,6 +3,7 @@ package com.flipperdevices.filemanager.upload.api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import com.flipperdevices.deeplink.model.DeeplinkContent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -29,24 +30,30 @@ interface UploaderDecomposeComponent { fun Render( state: State, speedState: Long?, - onCancel: () -> Unit, + onCancelClick: () -> Unit, modifier: Modifier ) sealed interface State { data object Pending : State data object Error : State - data object Uploaded : State + data class Uploaded(val items: List) : State data object Cancelled : State data class Uploading( - val fileIndex: Int, - val totalFiles: Int, - val uploadedFileSize: Long, - val uploadFileTotalSize: Long, - val fileName: String + val currentItemIndex: Int, + val totalItemsAmount: Int, + val uploadedSize: Long, + val totalSize: Long, + val currentItem: UploadingItem, ) : State } + data class UploadingItem( + val fileName: String, + val uploadedSize: Long, + val totalSize: Long + ) + fun interface Factory { operator fun invoke( componentContext: ComponentContext, diff --git a/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploadDecomposeComponentImpl.kt b/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploadDecomposeComponentImpl.kt index f83e32ac05..12cb518690 100644 --- a/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploadDecomposeComponentImpl.kt +++ b/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploadDecomposeComponentImpl.kt @@ -1,5 +1,7 @@ package com.flipperdevices.filemanager.upload.impl.api +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding @@ -9,41 +11,62 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.childContext import com.arkivanov.essenty.backhandler.BackCallback +import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType +import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.theme.LocalPalletV2 import com.flipperdevices.deeplink.api.DeepLinkParser +import com.flipperdevices.deeplink.model.Deeplink +import com.flipperdevices.filemanager.upload.api.MultipleFilesPicker import com.flipperdevices.filemanager.upload.api.UploadDecomposeComponent import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent -import com.flipperdevices.filemanager.upload.impl.composable.PickFilesEffect import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import me.gulya.anvil.assisted.ContributesAssistedFactory import okio.Path @ContributesAssistedFactory(AppGraph::class, UploadDecomposeComponent.Factory::class) class UploadDecomposeComponentImpl @AssistedInject constructor( @Assisted componentContext: ComponentContext, - @Assisted private val path: Path, - @Assisted private val onFinish: () -> Unit, + @Assisted private val onFilesChanged: (List) -> Unit, private val deepLinkParser: DeepLinkParser, uploaderDecomposeComponentFactory: UploaderDecomposeComponent.Factory -) : UploadDecomposeComponent(componentContext) { +) : ComponentContext by componentContext, + UploadDecomposeComponent { + private val uploaderDecomposeComponent = uploaderDecomposeComponentFactory.invoke( - componentContext = childContext("uploaderDecomposeComponent_$path") + componentContext = childContext("upload_dc_uploaderDecomposeComponent") ) private val backCallback = BackCallback { uploaderDecomposeComponent.onCancel() } - init { - backHandler.register(backCallback) + @Composable + override fun rememberMultipleFilesPicker(path: Path): MultipleFilesPicker { + val context = LocalContext.current + val pickFileLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.GetMultipleContents() + ) { uri -> + val deeplinkContents = runBlocking { + uri.map { deepLinkParser.fromUri(context, it) } + .filterIsInstance() + .mapNotNull { it.content } + } + if (deeplinkContents.isEmpty()) return@rememberLauncherForActivityResult + uploaderDecomposeComponent.tryUpload( + folderPath = path, + contents = deeplinkContents + ) + } + return MultipleFilesPicker { pickFileLauncher.launch("*/*") } } @Composable @@ -53,25 +76,35 @@ class UploadDecomposeComponentImpl @AssistedInject constructor( LaunchedEffect(uploaderDecomposeComponent) { uploaderDecomposeComponent.state - .filter { it !is UploaderDecomposeComponent.State.Uploading } - .filter { it !is UploaderDecomposeComponent.State.Pending } - .onEach { onFinish.invoke() } - .launchIn(this) + .onEach { + when (it) { + is UploaderDecomposeComponent.State.Uploaded, + is UploaderDecomposeComponent.State.Pending, + UploaderDecomposeComponent.State.Error, + UploaderDecomposeComponent.State.Cancelled -> { + if (backHandler.isRegistered(backCallback)) { + backHandler.unregister(backCallback) + } + } + + is UploaderDecomposeComponent.State.Uploading -> { + if (!backHandler.isRegistered(backCallback)) { + backHandler.register(backCallback) + } + val item = ListingItem( + fileName = it.currentItem.fileName, + fileType = FileType.FILE, + size = it.currentItem.uploadedSize + ) + onFilesChanged.invoke(listOf(item)) + } + } + }.launchIn(this) } - PickFilesEffect( - deepLinkParser = deepLinkParser, - onBack = uploaderDecomposeComponent::onCancel, - onContentsReady = { deeplinkContents -> - uploaderDecomposeComponent.tryUpload( - folderPath = path, - contents = deeplinkContents - ) - } - ) uploaderDecomposeComponent.Render( state = state, speedState = speedState, - onCancel = uploaderDecomposeComponent::onCancel, + onCancelClick = uploaderDecomposeComponent::onCancel, modifier = Modifier .fillMaxSize() .background(LocalPalletV2.current.surface.backgroundMain.body) 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 index b093e4e30a..3b4f35f766 100644 --- 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 @@ -3,15 +3,24 @@ 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( - fileName = "file_name.txt", - uploadedFileSize = 1234L, - uploadFileTotalSize = 1234567L, + 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/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/PickFilesEffect.kt b/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/PickFilesEffect.kt deleted file mode 100644 index df6ea6ec5d..0000000000 --- a/components/filemngr/upload/impl/src/androidMain/kotlin/com/flipperdevices/filemanager/upload/impl/composable/PickFilesEffect.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.flipperdevices.filemanager.upload.impl.composable - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import com.flipperdevices.deeplink.api.DeepLinkParser -import com.flipperdevices.deeplink.model.Deeplink -import com.flipperdevices.deeplink.model.DeeplinkContent -import kotlinx.coroutines.runBlocking - -@Composable -fun PickFilesEffect( - deepLinkParser: DeepLinkParser, - onBack: () -> Unit, - onContentsReady: (List) -> Unit -) { - val context = LocalContext.current - val pickFileLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetMultipleContents() - ) { uri -> - val deeplinkContents = runBlocking { - uri.map { deepLinkParser.fromUri(context, it) } - .filterIsInstance() - .mapNotNull { it.content } - } - if (deeplinkContents.isEmpty()) onBack.invoke() - onContentsReady.invoke(deeplinkContents) - } - - LaunchedEffect(pickFileLauncher) { - pickFileLauncher.launch("*/*") - } -} diff --git a/components/filemngr/upload/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/upload/impl/src/commonMain/composeResources/values/strings.xml index 72dccc7087..b17e630a4b 100644 --- a/components/filemngr/upload/impl/src/commonMain/composeResources/values/strings.xml +++ b/components/filemngr/upload/impl/src/commonMain/composeResources/values/strings.xml @@ -2,6 +2,8 @@ Size: %1$s of %2$s Speed: %1$s/s + %1$s/%2$s items Uploading... + Uploading: %1$s [%2$s/%3$s] Cancel Upload \ No newline at end of file diff --git a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploaderDecomposeComponentImpl.kt b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploaderDecomposeComponentImpl.kt index c572fdb43e..bf146ad197 100644 --- a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploaderDecomposeComponentImpl.kt +++ b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/api/UploaderDecomposeComponentImpl.kt @@ -51,7 +51,7 @@ class UploaderDecomposeComponentImpl @AssistedInject constructor( override fun Render( state: UploaderDecomposeComponent.State, speedState: Long?, - onCancel: () -> Unit, + onCancelClick: () -> Unit, modifier: Modifier ) { when (val localState = state) { @@ -59,7 +59,7 @@ class UploaderDecomposeComponentImpl @AssistedInject constructor( UploadingComposable( state = localState, speed = speedState, - onCancel = onCancel, + onCancel = onCancelClick, modifier = modifier ) } @@ -67,7 +67,7 @@ class UploaderDecomposeComponentImpl @AssistedInject constructor( UploaderDecomposeComponent.State.Cancelled, UploaderDecomposeComponent.State.Error, UploaderDecomposeComponent.State.Pending, - UploaderDecomposeComponent.State.Uploaded -> Unit + is UploaderDecomposeComponent.State.Uploaded -> Unit } } } 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 index 5e0c389e36..f1009f6cb3 100644 --- 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 @@ -21,20 +21,63 @@ 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( - fileName: String, - uploadedFileSize: Long, - uploadFileTotalSize: Long, + state: UploaderDecomposeComponent.State.Uploading, speed: Long?, ) { val animatedProgress by animateFloatAsState( - targetValue = if (uploadFileTotalSize == 0L) 0f else uploadedFileSize / uploadFileTotalSize.toFloat(), + targetValue = if (state.totalSize == 0L) 0f else state.uploadedSize / state.totalSize.toFloat(), animationSpec = tween(durationMillis = 500, easing = LinearEasing), label = "Progress" ) @@ -42,13 +85,7 @@ internal fun InProgressComposable( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = fileName, - style = LocalTypography.current.titleB18, - color = LocalPalletV2.current.text.title.primary, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) + InProgressTitleComposable(state) Spacer(Modifier.height(12.dp)) FlipperProgressIndicator( modifier = Modifier.padding(horizontal = 32.dp), @@ -58,11 +95,12 @@ internal fun InProgressComposable( percent = animatedProgress ) Spacer(Modifier.height(8.dp)) + InProgressDetailComposable(state) Text( text = stringResource( FUR.string.fm_in_progress_file_size, - uploadedFileSize.toFormattedSize(), - uploadFileTotalSize.toFormattedSize() + state.uploadedSize.toFormattedSize(), + state.totalSize.toFormattedSize() ), style = LocalTypography.current.subtitleM12, color = LocalPalletV2.current.text.body.secondary, 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 07ad2c0d47..c0c0606952 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 @@ -61,9 +61,7 @@ fun UploadingComposable( ) } InProgressComposable( - fileName = state.fileName, - uploadedFileSize = state.uploadedFileSize, - uploadFileTotalSize = state.uploadFileTotalSize, + state = state, speed = speed ) } diff --git a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/viewmodel/UploadViewModel.kt b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/viewmodel/UploadViewModel.kt index c43a3295cc..20e8afdda4 100644 --- a/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/viewmodel/UploadViewModel.kt +++ b/components/filemngr/upload/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/upload/impl/viewmodel/UploadViewModel.kt @@ -6,12 +6,15 @@ 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.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.core.FlipperStorageProvider import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import com.flipperdevices.core.progress.copyWithProgress import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel import com.flipperdevices.deeplink.model.DeeplinkContent +import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent.State import com.flipperdevices.filemanager.upload.impl.deeplink.DeeplinkContentProvider import kotlinx.coroutines.Job @@ -22,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow 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 @@ -68,26 +72,19 @@ class UploadViewModel @Inject constructor( deeplinkContent: DeeplinkContent, folderPath: Path, fileName: String? = deeplinkContent.filename(), - currentFileIndex: Int, - totalFilesAmount: Int - ) { - val fileStream = deeplinkContentProvider.source(deeplinkContent) ?: run { - error { "#uploadFile could not get deeplink source" } - _state.emit(State.Error) - return - } - val totalLength = deeplinkContent.length() ?: run { - error { "#uploadFile could not get deeplink totalLength" } - _state.emit(State.Error) - return - } - if (fileName == null) { - error { "#uploadFile fileName is null" } - _state.emit(State.Error) - return - } - val pathOnFlipper = folderPath.resolve(fileName) - runCatching { + ): Result { + return runCatching { + fileName ?: error("fileName is null") + val fileStream = deeplinkContentProvider + .source(deeplinkContent) + ?: error("Could not get deeplink source") + val totalLength = deeplinkContent + .length() + ?: error("Could not get deeplink totalLength") + check(state.first() is State.Uploading) { "State should be Uploading" } + + val pathOnFlipper = folderPath.resolve(fileName) + var prevProgressValue: Long = 0 fileStream.use { deeplinkSource -> uploadApi.sink(pathOnFlipper.toString()) .use { sink -> @@ -97,6 +94,8 @@ class UploadViewModel @Inject constructor( deeplinkContent.length() }, progressListener = { current, max -> + val diff = current - prevProgressValue + prevProgressValue = current if (!currentCoroutineContext().isActive) { sink.close() deeplinkSource.close() @@ -106,19 +105,27 @@ class UploadViewModel @Inject constructor( 0L -> 0f else -> current / max.toFloat() } - _state.update { - State.Uploading( - fileIndex = currentFileIndex, - totalFiles = totalFilesAmount, - uploadedFileSize = progress - .times(totalLength) - .toLong(), - uploadFileTotalSize = totalLength, - fileName = fileName + _state.update { state -> + val uploading = (state as? State.Uploading) + ?: error("Uploading state changed during file uploading") + uploading.copy( + uploadedSize = uploading.uploadedSize + diff, + currentItem = UploaderDecomposeComponent.UploadingItem( + fileName = fileName, + uploadedSize = progress + .times(totalLength) + .toLong(), + totalSize = totalLength + ) ) } } ) + ListingItem( + fileName = fileName, + fileType = FileType.FILE, + size = totalLength + ) } } }.onFailure { throwable -> error(throwable) { "#uploadFile could not upload file" } } @@ -138,11 +145,15 @@ class UploadViewModel @Inject constructor( ) _state.update { State.Uploading( - fileIndex = 0, - totalFiles = 1, - uploadedFileSize = 0L, - uploadFileTotalSize = 0L, - fileName = fileName + currentItemIndex = 0, + totalItemsAmount = 1, + uploadedSize = 0L, + totalSize = content.size.toLong(), + currentItem = UploaderDecomposeComponent.UploadingItem( + fileName = fileName, + uploadedSize = 0L, + totalSize = content.size.toLong() + ) ) } featureProvider.get() @@ -157,16 +168,14 @@ class UploadViewModel @Inject constructor( mutex.withLock("uploadRaw") { lastJob?.cancelAndJoin() lastJob = launch { - uploadFile( + val uploadedItems = uploadFile( uploadApi = uploadApi, deeplinkContent = deeplinkContent, fileName = fileName, folderPath = folderPath, - currentFileIndex = 0, - totalFilesAmount = 1 - ) + ).getOrNull().let(::listOf).filterNotNull() storageProvider.fileSystem.delete(temporaryFile) - _state.emit(State.Uploaded) + _state.emit(State.Uploaded(uploadedItems)) } } } @@ -182,13 +191,18 @@ class UploadViewModel @Inject constructor( return } val totalFilesAmount = contents.size + val totalSize = contents.sumOf { it.length() ?: 0L } _state.update { State.Uploading( - fileIndex = 0, - totalFiles = totalFilesAmount, - uploadedFileSize = 0L, - uploadFileTotalSize = 0L, - fileName = contents.firstOrNull()?.filename().orEmpty() + currentItemIndex = 0, + totalItemsAmount = totalFilesAmount, + uploadedSize = 0L, + totalSize = totalSize, + currentItem = UploaderDecomposeComponent.UploadingItem( + fileName = contents.firstOrNull()?.filename().orEmpty(), + uploadedSize = 0L, + totalSize = contents.firstOrNull()?.length() ?: 0L + ) ) } featureProvider.get() @@ -203,16 +217,23 @@ class UploadViewModel @Inject constructor( mutex.withLock("tryUpload") { lastJob?.cancelAndJoin() lastJob = launch { - contents.forEachIndexed { fileIndex, deeplinkContent -> + val uploadedItems = contents.mapIndexed { fileIndex, deeplinkContent -> + _state.update { state -> + val uploading = (state as? State.Uploading) + ?.copy(currentItemIndex = fileIndex) + if (uploading == null) { + error { "#tryUpload state was not uploading during $fileIndex'th uploading" } + } + uploading ?: state + } uploadFile( uploadApi = uploadApi, deeplinkContent = deeplinkContent, folderPath = folderPath, - currentFileIndex = fileIndex, - totalFilesAmount = totalFilesAmount - ) - } - _state.emit(State.Uploaded) + ).getOrNull() + }.filterNotNull() + + _state.emit(State.Uploaded(uploadedItems)) } } }