diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a335fe3f..9f74de5043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog -# 1.8.1 - In Progress +# 1.8.2 - In Progress + + +# 1.8.1 - [Feature] Add count subfolders for new file manager +- [Feature] Add file downloading for new file manager - [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/feature/storage/api/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/api/fm/FFileDownloadApi.kt b/components/bridge/connection/feature/storage/api/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/api/fm/FFileDownloadApi.kt index 423ade90a4..907fceb90e 100644 --- a/components/bridge/connection/feature/storage/api/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/api/fm/FFileDownloadApi.kt +++ b/components/bridge/connection/feature/storage/api/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/api/fm/FFileDownloadApi.kt @@ -2,6 +2,7 @@ package com.flipperdevices.bridge.connection.feature.storage.api.fm import com.flipperdevices.bridge.connection.feature.storage.api.model.StorageRequestPriority import com.flipperdevices.core.progress.FixedProgressListener +import kotlinx.coroutines.CoroutineScope import okio.Path import okio.Source @@ -15,6 +16,7 @@ interface FFileDownloadApi { fun source( pathOnFlipper: String, + scope: CoroutineScope, priority: StorageRequestPriority = StorageRequestPriority.DEFAULT ): Source } diff --git a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/FFileStorageApiFactoryImpl.kt b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/FFileStorageApiFactoryImpl.kt index 8831981cb6..2acf26934a 100644 --- a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/FFileStorageApiFactoryImpl.kt +++ b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/FFileStorageApiFactoryImpl.kt @@ -50,7 +50,7 @@ class FFileStorageApiFactoryImpl @Inject constructor() : FDeviceFeatureApi.Facto md5Api = FFileStorageMD5ApiImpl(rpcApi), fListingStorageApi = FListingStorageApiImpl(listingDelegate), fileUploadApi = FFileUploadApiImpl(rpcApi, scope = scope), - fileDownloadApi = FFileDownloadApiImpl(rpcApi, scope = scope), + fileDownloadApi = FFileDownloadApiImpl(rpcApi), deleteApi = FFileDeleteApiImpl(rpcApi), timestampApi = if (versionApi.isSupported(API_SUPPORTED_TIMESTAMP)) { FFileTimestampApiImpl(rpcApi) diff --git a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/FFileDownloadApiImpl.kt b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/FFileDownloadApiImpl.kt index eb8089b1d6..81f45070ab 100644 --- a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/FFileDownloadApiImpl.kt +++ b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/FFileDownloadApiImpl.kt @@ -23,7 +23,6 @@ import okio.use class FFileDownloadApiImpl( private val rpcFeatureApi: FRpcFeatureApi, - private val scope: CoroutineScope, private val fileSystem: FileSystem = FileSystem.SYSTEM ) : FFileDownloadApi { override suspend fun download( @@ -35,19 +34,24 @@ class FFileDownloadApiImpl( info { "Start download file $pathOnFlipper to $fileOnAndroid" } runCatching { + val sourceLength = getTotalSize(pathOnFlipper) fileSystem.sink(fileOnAndroid).buffer().use { sink -> - source(pathOnFlipper, priority).use { source -> + source(pathOnFlipper, this, priority).use { source -> source.copyWithProgress( - sink, - progressListener, - sourceLength = { getTotalSize(pathOnFlipper) } + sink = sink, + progressListener = progressListener, + sourceLength = { sourceLength } ) } } } } - override fun source(pathOnFlipper: String, priority: StorageRequestPriority): Source { + override fun source( + pathOnFlipper: String, + scope: CoroutineScope, + priority: StorageRequestPriority + ): Source { return FFlipperSource( readerLoop = ReaderRequestLooper( rpcFeatureApi = rpcFeatureApi, diff --git a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/ReaderRequestLooper.kt b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/ReaderRequestLooper.kt index e343a84388..92f2c0c382 100644 --- a/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/ReaderRequestLooper.kt +++ b/components/bridge/connection/feature/storage/impl/src/commonMain/kotlin/com/flipperdevices/bridge/connection/feature/storage/impl/fm/download/ReaderRequestLooper.kt @@ -6,9 +6,11 @@ import com.flipperdevices.bridge.connection.feature.rpc.model.wrapToRequest import com.flipperdevices.core.ktx.jre.FlipperDispatchers import com.flipperdevices.protobuf.Main import com.flipperdevices.protobuf.storage.ReadRequest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okio.Closeable @@ -17,7 +19,7 @@ class ReaderRequestLooper( private val rpcFeatureApi: FRpcFeatureApi, private val pathOnFlipper: String, private val priority: FlipperRequestPriority, - scope: CoroutineScope + private val scope: CoroutineScope ) : Closeable { private val queue = Channel
(Channel.UNLIMITED) private var isFinished = false @@ -41,8 +43,16 @@ class ReaderRequestLooper( } } + /** + * Implementation like this is required because after coroutine + * is cancelled, the queue.receive() will lasts forever + */ suspend fun getNextBytePack(): Main { - return queue.receive() + while (scope.isActive) { + val value = queue.tryReceive().getOrNull() + if (value != null) return value + } + throw CancellationException("Scope got cancelled during getting next byte pack") } override fun close() { diff --git a/components/bridge/connection/sample/build.gradle.kts b/components/bridge/connection/sample/build.gradle.kts index 559106fa96..31b09ad358 100644 --- a/components/bridge/connection/sample/build.gradle.kts +++ b/components/bridge/connection/sample/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.components.core.log) implementation(projects.components.core.preference) implementation(projects.components.core.storage) + implementation(projects.components.core.share) implementation(projects.components.bridge.connection.transport.ble.api) implementation(projects.components.bridge.connection.transport.ble.impl) @@ -86,6 +87,8 @@ dependencies { implementation(projects.components.filemngr.search.impl) implementation(projects.components.filemngr.editor.api) implementation(projects.components.filemngr.editor.impl) + implementation(projects.components.filemngr.download.api) + implementation(projects.components.filemngr.download.impl) implementation(projects.components.newfilemanager.api) implementation(projects.components.newfilemanager.impl) diff --git a/components/core/progress/build.gradle.kts b/components/core/progress/build.gradle.kts index 3372030175..a0deb31064 100644 --- a/components/core/progress/build.gradle.kts +++ b/components/core/progress/build.gradle.kts @@ -8,6 +8,7 @@ android.namespace = "com.flipperdevices.core.progress" commonDependencies { implementation(projects.components.core.buildKonfig) implementation(libs.okio) + implementation(libs.kotlin.coroutines) } commonTestDependencies { diff --git a/components/core/progress/src/commonMain/kotlin/com/flipperdevices/core/progress/SourceKtx.kt b/components/core/progress/src/commonMain/kotlin/com/flipperdevices/core/progress/SourceKtx.kt index 09f206f395..61e03e7b51 100644 --- a/components/core/progress/src/commonMain/kotlin/com/flipperdevices/core/progress/SourceKtx.kt +++ b/components/core/progress/src/commonMain/kotlin/com/flipperdevices/core/progress/SourceKtx.kt @@ -1,5 +1,7 @@ package com.flipperdevices.core.progress +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive import okio.Buffer import okio.Sink import okio.Source @@ -20,7 +22,7 @@ suspend fun Source.copyWithProgress( var totalBytesRead = 0L val buffer = Buffer() - while (true) { + while (currentCoroutineContext().isActive) { val readCount: Long = read(buffer, chunkSize) if (readCount == -1L) break sink.write(buffer, readCount) diff --git a/components/core/share/build.gradle.kts b/components/core/share/build.gradle.kts index c5bc3e7610..6436f65b40 100644 --- a/components/core/share/build.gradle.kts +++ b/components/core/share/build.gradle.kts @@ -1,5 +1,7 @@ plugins { - id("flipper.android-lib") + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") } android.namespace = "com.flipperdevices.core.share" @@ -13,7 +15,12 @@ android { } } -dependencies { +commonDependencies { + implementation(projects.components.core.di) + implementation(libs.okio) +} + +androidDependencies { implementation(projects.components.core.ktx) implementation(libs.annotations) implementation(libs.appcompat) diff --git a/components/core/share/src/main/AndroidManifest.xml b/components/core/share/src/androidMain/AndroidManifest.xml similarity index 100% rename from components/core/share/src/main/AndroidManifest.xml rename to components/core/share/src/androidMain/AndroidManifest.xml 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 new file mode 100644 index 0000000000..35506eb4c2 --- /dev/null +++ b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/AndroidShareHelper.kt @@ -0,0 +1,26 @@ +package com.flipperdevices.core.share + +import android.content.Context +import com.flipperdevices.core.di.AppGraph +import com.squareup.anvil.annotations.ContributesBinding +import okio.Path.Companion.toOkioPath +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, PlatformShareHelper::class) +class AndroidShareHelper @Inject constructor( + private val context: Context +) : PlatformShareHelper { + + override fun provideSharableFile(fileName: String): PlatformSharableFile { + val path = SharableFile(context, fileName).toOkioPath() + return PlatformSharableFile(path) + } + + override fun shareFile(file: PlatformSharableFile, title: String) { + ShareHelper.shareFile( + context = context, + file = file, + text = title + ) + } +} diff --git a/components/core/share/src/main/kotlin/com/flipperdevices/core/share/SharableFile.kt b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/SharableFile.kt similarity index 100% rename from components/core/share/src/main/kotlin/com/flipperdevices/core/share/SharableFile.kt rename to components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/SharableFile.kt diff --git a/components/core/share/src/main/kotlin/com/flipperdevices/core/share/ShareHelper.kt b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/ShareHelper.kt similarity index 81% rename from components/core/share/src/main/kotlin/com/flipperdevices/core/share/ShareHelper.kt rename to components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/ShareHelper.kt index 668980e6a5..21a51ed420 100644 --- a/components/core/share/src/main/kotlin/com/flipperdevices/core/share/ShareHelper.kt +++ b/components/core/share/src/androidMain/kotlin/com/flipperdevices/core/share/ShareHelper.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import androidx.core.content.FileProvider import com.flipperdevices.core.ktx.jre.createNewFileWithMkDirs +import okio.Path.Companion.toOkioPath object ShareHelper { fun shareRawFile(context: Context, data: ByteArray, resId: Int, name: String) { @@ -22,12 +23,13 @@ object ShareHelper { resId = resId ) } - fun shareFile(context: Context, file: SharableFile, resId: Int) { + + fun shareFile(context: Context, file: PlatformSharableFile, text: String) { val uri = FileProvider.getUriForFile( context, BuildConfig.SHARE_FILE_AUTHORITIES, - file, - file.name + file.path.toFile(), + file.path.name ) val intent = Intent(Intent.ACTION_SEND, uri).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -35,13 +37,21 @@ object ShareHelper { } val activityIntent = Intent.createChooser( intent, - context.getString(resId, file.name) + text ).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(activityIntent) } + fun shareFile(context: Context, file: SharableFile, resId: Int) { + shareFile( + context = context, + file = PlatformSharableFile(file.toOkioPath()), + text = context.getString(resId, file.name) + ) + } + fun shareText( context: Context, title: String, diff --git a/components/core/share/src/main/res/xml/filepaths.xml b/components/core/share/src/androidMain/res/xml/filepaths.xml similarity index 100% rename from components/core/share/src/main/res/xml/filepaths.xml rename to components/core/share/src/androidMain/res/xml/filepaths.xml diff --git a/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformSharableFile.kt b/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformSharableFile.kt new file mode 100644 index 0000000000..21de876c90 --- /dev/null +++ b/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformSharableFile.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.core.share + +import okio.Path + +/** + * This class should be only created via [PlatformShareHelper] due + * to permission restrictions. On android we can share files only in + * specified folder specified in AndroidManifest + */ +class PlatformSharableFile internal constructor(val path: Path) diff --git a/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformShareHelper.kt b/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformShareHelper.kt new file mode 100644 index 0000000000..e35a0ece2d --- /dev/null +++ b/components/core/share/src/commonMain/kotlin/com/flipperdevices/core/share/PlatformShareHelper.kt @@ -0,0 +1,16 @@ +package com.flipperdevices.core.share + +interface PlatformShareHelper { + /** + * Provide file which can be shared later via [shareFile] + * + * If file with same name [fileName] exists, it will be deleted + */ + fun provideSharableFile(fileName: String): PlatformSharableFile + + /** + * @param file file to share + * @param title text displayed as hint for user + */ + fun shareFile(file: PlatformSharableFile, title: String) +} diff --git a/components/core/share/src/desktopMain/kotlin/com/flipperdevices/core/share/DesktopShareHelper.kt b/components/core/share/src/desktopMain/kotlin/com/flipperdevices/core/share/DesktopShareHelper.kt new file mode 100644 index 0000000000..3fe52f9d6a --- /dev/null +++ b/components/core/share/src/desktopMain/kotlin/com/flipperdevices/core/share/DesktopShareHelper.kt @@ -0,0 +1,17 @@ +package com.flipperdevices.core.share + +import com.flipperdevices.core.di.AppGraph +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +@ContributesBinding(AppGraph::class, PlatformShareHelper::class) +class DesktopShareHelper @Inject constructor() : PlatformShareHelper { + + override fun provideSharableFile(fileName: String): PlatformSharableFile { + error("The desktop feature is not yet implemented!") + } + + override fun shareFile(file: PlatformSharableFile, title: String) { + error("The desktop feature is not yet implemented!") + } +} diff --git a/components/filemngr/download/api/build.gradle.kts b/components/filemngr/download/api/build.gradle.kts new file mode 100644 index 0000000000..65700f8b18 --- /dev/null +++ b/components/filemngr/download/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("flipper.multiplatform") + id("flipper.multiplatform-dependencies") +} + +android.namespace = "com.flipperdevices.filemanager.download.api" + +commonDependencies { + implementation(projects.components.core.ui.decompose) + + implementation(libs.compose.ui) + implementation(libs.decompose) + + implementation(libs.okio) +} 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 new file mode 100644 index 0000000000..495869a718 --- /dev/null +++ b/components/filemngr/download/api/src/commonMain/kotlin/com/flipperdevices/filemanager/download/api/DownloadDecomposeComponent.kt @@ -0,0 +1,25 @@ +package com.flipperdevices.filemanager.download.api + +import com.arkivanov.decompose.ComponentContext +import com.flipperdevices.ui.decompose.ScreenDecomposeComponent +import kotlinx.coroutines.flow.StateFlow +import okio.Path + +abstract class DownloadDecomposeComponent( + componentContext: ComponentContext +) : ScreenDecomposeComponent(componentContext) { + abstract val isInProgress: StateFlow + + abstract fun onCancel() + + abstract fun download( + fullPath: Path, + size: Long + ) + + fun interface Factory { + operator fun invoke( + componentContext: ComponentContext, + ): DownloadDecomposeComponent + } +} diff --git a/components/filemngr/download/impl/build.gradle.kts b/components/filemngr/download/impl/build.gradle.kts new file mode 100644 index 0000000000..4b1450333f --- /dev/null +++ b/components/filemngr/download/impl/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("flipper.multiplatform-compose") + id("flipper.multiplatform-dependencies") + id("flipper.anvil-multiplatform") + id("kotlinx-serialization") +} +android.namespace = "com.flipperdevices.filemanager.download.impl" + +commonDependencies { + implementation(projects.components.core.di) + implementation(projects.components.core.ktx) + implementation(projects.components.core.log) + + 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.share) + implementation(projects.components.core.storage) + implementation(projects.components.core.progress) + + 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.download.api) + + // Compose + implementation(libs.compose.ui) + implementation(libs.compose.tooling) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + + 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/download/impl/src/commonMain/composeResources/values/strings.xml b/components/filemngr/download/impl/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000000..f91e5562a6 --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,10 @@ + + + Size: %1$s of %2$s + Speed: %1$s/s + %1$s/%2$s items + Downloading... + Downloading: %1$s [%2$s/%3$s] + Cancel Download + Share downloaded File + \ No newline at end of file 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 new file mode 100644 index 0000000000..7c4ada5d24 --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt @@ -0,0 +1,79 @@ +package com.flipperdevices.filemanager.download.impl.api + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.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 +import com.flipperdevices.filemanager.download.impl.model.DownloadableFile +import com.flipperdevices.filemanager.download.impl.viewmodel.DownloadViewModel +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) +class DownloadDecomposeComponentImpl @AssistedInject constructor( + @Assisted componentContext: ComponentContext, + private val downloadViewModelFactory: Provider, + private val platformShareHelper: PlatformShareHelper +) : DownloadDecomposeComponent(componentContext) { + private val downloadViewModel = instanceKeeper.getOrCreate { + downloadViewModelFactory.get() + } + + override val isInProgress = downloadViewModel.state + .map { state -> state is DownloadViewModel.State.Downloading } + .stateIn(coroutineScope(), SharingStarted.Eagerly, false) + + override fun onCancel() = downloadViewModel.onCancel() + + override fun download( + fullPath: Path, + size: Long + ) = downloadViewModel.tryDownload( + file = DownloadableFile( + fullPath = fullPath, + size = size + ) + ) + + @Composable + override fun Render() { + val state by downloadViewModel.state.collectAsState() + when (val localState = state) { + is DownloadViewModel.State.Downloading -> { + DownloadingComposable( + state = localState, + onCancel = downloadViewModel::onCancel, + modifier = Modifier + .fillMaxSize() + .clickable(enabled = false, onClick = {}) + .background(LocalPalletV2.current.surface.backgroundMain.body) + .navigationBarsPadding() + .systemBarsPadding(), + ) + } + + DownloadViewModel.State.Error, + DownloadViewModel.State.Pending, + DownloadViewModel.State.NotSupported -> Unit + } + } +} 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 new file mode 100644 index 0000000000..3bfe684da1 --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/DownloadingComposable.kt @@ -0,0 +1,66 @@ +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.filemanager.download.impl.viewmodel.DownloadViewModel +import flipperapp.components.filemngr.download.impl.generated.resources.fm_cancel +import flipperapp.components.filemngr.download.impl.generated.resources.fm_downloading +import org.jetbrains.compose.resources.stringResource +import flipperapp.components.filemngr.download.impl.generated.resources.Res as FDR + +@Composable +fun DownloadingComposable( + state: DownloadViewModel.State.Downloading, + 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 + ) + } + 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 new file mode 100644 index 0000000000..3fcb6fcb24 --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/composable/InProgressComposable.kt @@ -0,0 +1,111 @@ +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 new file mode 100644 index 0000000000..dbd48c624b --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/model/DownloadableFile.kt @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000000..06854b59dc --- /dev/null +++ b/components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/viewmodel/DownloadViewModel.kt @@ -0,0 +1,176 @@ +package com.flipperdevices.filemanager.download.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.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.share.PlatformShareHelper +import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel +import com.flipperdevices.filemanager.download.impl.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 +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +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.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import okio.Path +import org.jetbrains.compose.resources.getString +import javax.inject.Inject + +class DownloadViewModel @Inject constructor( + private val featureProvider: FFeatureProvider, + private val platformShareHelper: PlatformShareHelper +) : DecomposeViewModel(), LogTagProvider { + override val TAG: String = "DownloadViewModel" + + private val _state = MutableStateFlow(State.Pending) + val state = _state.asStateFlow() + + private val mutex = Mutex() + + private var _featureJob: Job? = null + + private suspend fun download(storageFeatureApi: FStorageFeatureApi, flipperFileFullPath: Path) { + val pathOnAndroid = platformShareHelper.provideSharableFile(flipperFileFullPath.name) + storageFeatureApi.downloadApi().download( + pathOnFlipper = flipperFileFullPath.toString(), + fileOnAndroid = pathOnAndroid.path, + progressListener = { current, max -> + println("DownloadViewModel progressListener") + _state.update { state -> + (state as? State.Downloading) + ?.copy(downloadedSize = current, totalSize = max) + ?: State.Downloading( + downloadedSize = current, + totalSize = max, + downloadSpeed = 0L, + fullPath = flipperFileFullPath + ) + } + } + ).onFailure { exception -> + error(exception) { "Can't download $flipperFileFullPath" } + _state.emit(State.Error) + }.onSuccess { + withContext(Dispatchers.Main) { + platformShareHelper.shareFile( + file = pathOnAndroid, + title = getString(Res.string.fm_share_title) + ) + } + _state.emit(State.Pending) + viewModelScope.launch { _featureJob?.cancelAndJoin() } + } + } + + fun onCancel() { + viewModelScope.launch { + println("DownloadViewModel cancelling") + _featureJob?.cancelAndJoin() + println("DownloadViewModel cancelled") + _state.emit(State.Pending) + } + } + + fun tryDownload(file: DownloadableFile) { + viewModelScope.launch { + _featureJob?.cancelAndJoin() + + mutex.withLock { + _featureJob = featureProvider.get() + .onEach { storageFeatureStatus -> + when (storageFeatureStatus) { + FFeatureStatus.NotFound, + FFeatureStatus.Unsupported -> _state.emit(State.NotSupported) + + FFeatureStatus.Retrieving -> { + _state.emit( + State.Downloading( + downloadedSize = 0L, + totalSize = file.size, + downloadSpeed = 0L, + fullPath = file.fullPath + ) + ) + } + + is FFeatureStatus.Supported -> { + _state.emit( + State.Downloading( + downloadedSize = 0L, + totalSize = file.size, + downloadSpeed = 0L, + fullPath = file.fullPath + ) + ) + download( + storageFeatureApi = storageFeatureStatus.featureApi, + flipperFileFullPath = file.fullPath + ) + } + } + }.catch { it.printStackTrace() }.launchIn(viewModelScope) + _featureJob?.join() + println("DownloadViewModel out of mutex") + } + } + } + + private fun collectSpeedState() { + featureProvider.get() + .flatMapLatest { storageFeatureStatus -> + when (storageFeatureStatus) { + is FFeatureStatus.Supported -> + storageFeatureStatus.featureApi + .getSpeed() + .map { fSerialSpeed -> fSerialSpeed.receiveBytesInSec } + + FFeatureStatus.Retrieving, + FFeatureStatus.NotFound, + FFeatureStatus.Unsupported -> flowOf(0L) + } + } + .filter { _featureJob?.isActive == true } + .onEach { speed -> + _state.update { state -> + (state as? State.Downloading) + ?.copy(downloadSpeed = speed) + ?: state + } + } + .launchIn(viewModelScope) + } + + init { + collectSpeedState() + } + + sealed interface State { + data object Pending : State + data object NotSupported : State + data object Error : State + data class Downloading( + val downloadedSize: Long, + val fullPath: Path, + val totalSize: Long, + val downloadSpeed: Long + ) : State + } +} diff --git a/components/filemngr/listing/impl/build.gradle.kts b/components/filemngr/listing/impl/build.gradle.kts index 50b95cd873..57d367d198 100644 --- a/components/filemngr/listing/impl/build.gradle.kts +++ b/components/filemngr/listing/impl/build.gradle.kts @@ -32,6 +32,7 @@ commonDependencies { implementation(projects.components.filemngr.listing.api) implementation(projects.components.filemngr.main.api) implementation(projects.components.filemngr.upload.api) + implementation(projects.components.filemngr.download.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 b322b458f6..273a08e4a9 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 @@ -12,6 +12,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.download.api.DownloadDecomposeComponent import com.flipperdevices.filemanager.listing.api.FilesDecomposeComponent import com.flipperdevices.filemanager.listing.impl.composable.ComposableFileListScreen import com.flipperdevices.filemanager.listing.impl.composable.LaunchedEventsComposable @@ -45,6 +46,7 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( 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, ) : FilesDecomposeComponent(componentContext) { @@ -71,6 +73,11 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( onFilesChanged = filesViewModel::onFilesChanged, ) } + private val downloadDecomposeComponent by lazy { + downloadDecomposeComponentFactory.invoke( + componentContext = childContext("FilesDecomposeComponent_downloadDecomposeComponent") + ) + } private val backCallback = BackCallback { val parent = path.parent @@ -79,16 +86,16 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( selectionViewModel.toggleMode() } - parent == null -> { - onBack.invoke() + downloadDecomposeComponent.isInProgress.value -> { + downloadDecomposeComponent.onCancel() } - parent != null -> { - pathChangedCallback.invoke(parent) + parent == null -> { + onBack.invoke() } else -> { - onBack.invoke() + pathChangedCallback.invoke(parent) } } } @@ -138,8 +145,10 @@ class FilesDecomposeComponentImpl @AssistedInject constructor( slotNavigation = slotNavigation, selectionViewModel = selectionViewModel, createFileViewModel = createFileViewModel, - deleteFileViewModel = deleteFileViewModel + deleteFileViewModel = deleteFileViewModel, + onDownloadFile = downloadDecomposeComponent::download ) uploadDecomposeComponent.Render() + downloadDecomposeComponent.Render() } } 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 fb10cf83ef..b6fed028a6 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 @@ -43,24 +43,29 @@ fun LazyGridScope.LoadedFilesComposable( val isFileLoading = remember(deleteFileState.fileNamesOrNull) { deleteFileState.fileNamesOrNull.orEmpty().contains(file.itemName) } - Crossfade(isFileLoading) { animatedIsFileLoading -> + Crossfade( + targetState = isFileLoading, + modifier = Modifier.animateItem() + ) { animatedIsFileLoading -> if (animatedIsFileLoading) { FolderCardPlaceholderComposable( modifier = Modifier .fillMaxWidth() - .animateItem() .animateContentSize(), orientation = orientation, ) } else { val filePathWithType = remember(path, file.itemName) { val fullPath = path.resolve(file.itemName) - PathWithType(file.itemType, fullPath) + PathWithType( + fileType = file.itemType, + fullPath = fullPath, + size = file.sizeOrNull ?: 0 + ) } FolderCardComposable( modifier = Modifier .fillMaxWidth() - .animateItem() .animateContentSize(), painter = file.asListingItem().asPainter(), iconTint = file.asListingItem().asTint(), 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 854467e28a..dc1ad58d5f 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 @@ -71,7 +71,8 @@ fun FileListAppBar( .map { PathWithType( fileType = it.itemType, - fullPath = path.resolve(it.itemName) + fullPath = path.resolve(it.itemName), + size = it.sizeOrNull ?: 0 ) } selectionViewModel.select(paths) 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 12ac6b9d48..79373ec86b 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 @@ -85,25 +85,29 @@ fun BottomSheetOptionsContent( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_copy_to), painter = painterResource(FR.drawable.ic_copy_to), - onClick = onCopyTo + onClick = onCopyTo, + isEnabled = false ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_move_to), painter = painterResource(FR.drawable.ic_move), - onClick = onMoveTo + onClick = onMoveTo, + isEnabled = false ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_export), painter = painterResource(FR.drawable.ic_upload), - onClick = onExport + onClick = onExport, + isEnabled = fileType == FileType.FILE ) HorizontalTextIconButton( modifier = Modifier.fillMaxWidth(), text = stringResource(FML.string.fml_rename), painter = painterResource(FR.drawable.ic_edit), - onClick = onRename + onClick = onRename, + 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 c6f05f407e..f8034acb50 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 @@ -11,6 +11,7 @@ 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( @@ -19,28 +20,32 @@ fun FileOptionsBottomSheet( slotNavigation: SlotNavigation, selectionViewModel: SelectionViewModel, deleteFileViewModel: DeleteFilesViewModel, + onDownloadFile: (Path, Long) -> Unit, modifier: Modifier = Modifier ) { SlotModalBottomSheet( childSlotValue = fileOptionsSlot, onDismiss = { slotNavigation.dismiss() }, - content = { + content = { pathWithType -> BottomSheetOptionsContent( modifier = modifier.navigationBarsPadding(), - fileType = it.fileType, - path = it.fullPath, + fileType = pathWithType.fileType, + path = pathWithType.fullPath, onCopyTo = {}, // todo onSelect = { - selectionViewModel.select(it) + selectionViewModel.select(pathWithType) slotNavigation.dismiss() }, onRename = { - createFileViewModel.onRename(it) + createFileViewModel.onRename(pathWithType) + slotNavigation.dismiss() + }, + onExport = { + onDownloadFile.invoke(pathWithType.fullPath, pathWithType.size) slotNavigation.dismiss() }, - onExport = {}, // todo onDelete = { - deleteFileViewModel.tryDelete(it.fullPath) + deleteFileViewModel.tryDelete(pathWithType.fullPath) slotNavigation.dismiss() }, onMoveTo = {} // todo diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/HorizontalTextIconButton.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/HorizontalTextIconButton.kt index 9ced3e50c3..b62c8f8533 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/HorizontalTextIconButton.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/composable/modal/HorizontalTextIconButton.kt @@ -24,8 +24,11 @@ fun HorizontalTextIconButton( painter: Painter, onClick: () -> Unit, modifier: Modifier = Modifier, + isEnabled: Boolean = true, iconTint: Color = LocalPalletV2.current.icon.blackAndWhite.default, - textColor: Color = LocalPalletV2.current.action.blackAndWhite.text.default + iconDisabledTint: Color = LocalPalletV2.current.action.blackAndWhite.icon.disabled, + textColor: Color = LocalPalletV2.current.action.blackAndWhite.text.default, + textDisabledColor: Color = LocalPalletV2.current.action.blackAndWhite.text.disabled ) { Row( modifier = modifier @@ -37,14 +40,14 @@ fun HorizontalTextIconButton( ) { Icon( painter = painter, - tint = iconTint, + tint = if (isEnabled) iconTint else iconDisabledTint, modifier = Modifier.size(24.dp), contentDescription = null ) Text( text = text, style = LocalTypography.current.subtitleM12, - color = textColor + color = if (isEnabled) textColor else textDisabledColor ) } } 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 11f856365d..d964f2360e 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 @@ -76,7 +76,8 @@ private fun MoreBottomBarOptions( IconDropdownItem( text = stringResource(FML.string.fml_copy_to), painter = painterResource(FR.drawable.ic_copy_to), - onClick = onCopyTo + onClick = onCopyTo, + isActive = false ) } } @@ -118,7 +119,7 @@ fun BottomBarOptions( VerticalTextIconButton( text = stringResource(FML.string.fml_move), painter = painterResource(FR.drawable.ic_move), - onClick = onMove + onClick = onMove, ) VerticalTextIconButton( 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 869cd51c71..d6ccd4bd42 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 @@ -4,11 +4,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon 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.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp @@ -22,11 +24,15 @@ fun VerticalTextIconButton( painter: Painter, onClick: () -> Unit, modifier: Modifier = Modifier, + isEnabled: Boolean = true, iconTint: Color = LocalPalletV2.current.icon.blackAndWhite.default, - textColor: Color = LocalPalletV2.current.action.blackAndWhite.text.default + iconDisabledTint: Color = LocalPalletV2.current.action.blackAndWhite.icon.disabled, + textColor: Color = LocalPalletV2.current.action.blackAndWhite.text.default, + textDisabledColor: Color = LocalPalletV2.current.action.blackAndWhite.text.disabled ) { Column( modifier = modifier + .clip(RoundedCornerShape(12.dp)) .clickableRipple(onClick = onClick) .padding(vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(2.dp), @@ -34,14 +40,14 @@ fun VerticalTextIconButton( ) { Icon( painter = painter, - tint = iconTint, + tint = if (isEnabled) iconTint else iconDisabledTint, modifier = Modifier.size(24.dp), contentDescription = null ) Text( text = text, style = LocalTypography.current.subtitleM12, - color = textColor + color = if (isEnabled) textColor else textDisabledColor ) } } diff --git a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt index c9a92778cb..1f3808e03e 100644 --- a/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt +++ b/components/filemngr/listing/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/listing/impl/model/ExtendedListingItem.kt @@ -16,6 +16,9 @@ sealed interface ExtendedListingItem { val itemName: String get() = path.name + val sizeOrNull: Long? + get() = (this as? File)?.size + fun asListingItem() = ListingItem( fileName = itemName, fileType = itemType, 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 bd5d63f075..c64c772919 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 @@ -9,5 +9,6 @@ import okio.Path data class PathWithType( val fileType: FileType, @Serializable(PathSerializer::class) - val fullPath: Path + val fullPath: Path, + val size: Long ) diff --git a/settings.gradle.kts b/settings.gradle.kts index ee133696a3..bfae6156c8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -101,6 +101,8 @@ include( ":components:filemngr:search:impl", ":components:filemngr:editor:api", ":components:filemngr:editor:impl", + ":components:filemngr:download:api", + ":components:filemngr:download:impl", ":components:core:di", ":components:core:ktx",