Skip to content

Commit

Permalink
Improve file edit screen (#996)
Browse files Browse the repository at this point in the history
**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
  • Loading branch information
makeevrserg authored Dec 2, 2024
1 parent 8b2440a commit f234ff0
Show file tree
Hide file tree
Showing 30 changed files with 1,108 additions and 375 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +31,6 @@ import javax.inject.Provider
class DownloadDecomposeComponentImpl @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
private val downloadViewModelFactory: Provider<DownloadViewModel>,
private val platformShareHelper: PlatformShareHelper
) : DownloadDecomposeComponent(componentContext) {
private val downloadViewModel = instanceKeeper.getOrCreate {
downloadViewModelFactory.get()
Expand Down
1 change: 1 addition & 0 deletions components/filemngr/editor/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<C : Any> : CompositeDecomposeComponent<C>() {
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
onBack: DecomposeOnBackParameter,
onFileChanged: (ListingItem) -> Unit,
path: Path
): FileManagerEditorDecomposeComponent
): FileManagerEditorDecomposeComponent<*>
}
}
1 change: 0 additions & 1 deletion components/filemngr/editor/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<string name="fme_too_large_file">The file is larger than 1MB and therefore only part of the file is shown.</string>
<string name="fme_save_as_file">Save File as...</string>
<string name="fme_save">Save</string>
<string name="fme_status_downloading">Downloading...</string>
<string name="fme_status_uploading">Uploading: %1$s [%2$s/%3$s]</string>
<string name="fme_status_speed">Speed: %1$s/s</string>
<string name="fme_cancel">Cancel</string>
<string name="fme_allowed_characters">Allowed characters: %1$s</string>
<string name="fme_txt">TXT</string>
<string name="fme_hex">HEX</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<SlotConfiguration>()

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)
}
}
Original file line number Diff line number Diff line change
@@ -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<DownloadViewModel.State.Downloaded>()
.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
}
}
Loading

0 comments on commit f234ff0

Please sign in to comment.