Skip to content

Commit f234ff0

Browse files
authored
Improve file edit screen (#996)
**Background** File editor currently contains a lot of logic, which makes code reading harder. It can be improved by splitting it into separate decompose screen for each task: Loading, Editing, Uploading **Changes** - Split editor into multiple decompose screens **Test plan** - Open new editor. See new beautiful new loading screen with progress. - After it loaded try edit your file - After finish try save file or save file as new file. - See new file immediately appeared in listing with updated size
1 parent 8b2440a commit f234ff0

File tree

30 files changed

+1108
-375
lines changed

30 files changed

+1108
-375
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Feature] Add count subfolders for new file manager
99
- [Feature] Add file downloading for new file manager
1010
- [Refactor] Move rename and file create to separated modules
11+
- [Refactor] Improve and refactor new FileManager Editor
1112
- [FIX] Migrate url host from metric.flipperdevices.com to metric.flipp.dev
1213
- [FIX] Fix empty response in faphub category
1314
- [FIX] New file manager uploading progress

components/filemngr/download/impl/src/commonMain/kotlin/com/flipperdevices/filemanager/download/impl/api/DownloadDecomposeComponentImpl.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import com.arkivanov.decompose.ComponentContext
1313
import com.arkivanov.essenty.instancekeeper.getOrCreate
1414
import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope
1515
import com.flipperdevices.core.di.AppGraph
16-
import com.flipperdevices.core.share.PlatformShareHelper
1716
import com.flipperdevices.core.ui.theme.LocalPalletV2
1817
import com.flipperdevices.filemanager.download.api.DownloadDecomposeComponent
1918
import com.flipperdevices.filemanager.download.impl.composable.DownloadingComposable
@@ -32,7 +31,6 @@ import javax.inject.Provider
3231
class DownloadDecomposeComponentImpl @AssistedInject constructor(
3332
@Assisted componentContext: ComponentContext,
3433
private val downloadViewModelFactory: Provider<DownloadViewModel>,
35-
private val platformShareHelper: PlatformShareHelper
3634
) : DownloadDecomposeComponent(componentContext) {
3735
private val downloadViewModel = instanceKeeper.getOrCreate {
3836
downloadViewModelFactory.get()

components/filemngr/editor/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ android.namespace = "com.flipperdevices.filemanager.editor.api"
77

88
commonDependencies {
99
implementation(projects.components.core.ui.decompose)
10+
implementation(projects.components.bridge.connection.feature.storage.api)
1011

1112
implementation(libs.compose.ui)
1213
implementation(libs.decompose)
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package com.flipperdevices.filemanager.editor.api
22

33
import com.arkivanov.decompose.ComponentContext
4+
import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem
5+
import com.flipperdevices.ui.decompose.CompositeDecomposeComponent
46
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
5-
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent
67
import okio.Path
78

8-
abstract class FileManagerEditorDecomposeComponent(
9-
componentContext: ComponentContext
10-
) : ScreenDecomposeComponent(componentContext) {
9+
abstract class FileManagerEditorDecomposeComponent<C : Any> : CompositeDecomposeComponent<C>() {
1110
fun interface Factory {
1211
operator fun invoke(
1312
componentContext: ComponentContext,
1413
onBack: DecomposeOnBackParameter,
14+
onFileChanged: (ListingItem) -> Unit,
1515
path: Path
16-
): FileManagerEditorDecomposeComponent
16+
): FileManagerEditorDecomposeComponent<*>
1717
}
1818
}

components/filemngr/editor/impl/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ commonDependencies {
3434

3535
implementation(projects.components.filemngr.uiComponents)
3636
implementation(projects.components.filemngr.editor.api)
37-
implementation(projects.components.filemngr.upload.api)
3837
implementation(projects.components.filemngr.main.api)
3938
implementation(projects.components.filemngr.util)
4039

components/filemngr/editor/impl/src/commonMain/composeResources/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<string name="fme_too_large_file">The file is larger than 1MB and therefore only part of the file is shown.</string>
44
<string name="fme_save_as_file">Save File as...</string>
55
<string name="fme_save">Save</string>
6+
<string name="fme_status_downloading">Downloading...</string>
7+
<string name="fme_status_uploading">Uploading: %1$s [%2$s/%3$s]</string>
8+
<string name="fme_status_speed">Speed: %1$s/s</string>
9+
<string name="fme_cancel">Cancel</string>
610
<string name="fme_allowed_characters">Allowed characters: %1$s</string>
711
<string name="fme_txt">TXT</string>
812
<string name="fme_hex">HEX</string>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.flipperdevices.filemanager.editor.api
2+
3+
import androidx.compose.runtime.Composable
4+
import com.arkivanov.decompose.ComponentContext
5+
import com.arkivanov.essenty.instancekeeper.getOrCreate
6+
import com.flipperdevices.filemanager.editor.composable.dialog.CreateFileDialogComposable
7+
import com.flipperdevices.filemanager.editor.viewmodel.EditFileNameViewModel
8+
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
9+
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent
10+
import dagger.assisted.Assisted
11+
import dagger.assisted.AssistedFactory
12+
import dagger.assisted.AssistedInject
13+
import okio.Path
14+
15+
class EditFileNameDecomposeComponent @AssistedInject constructor(
16+
@Assisted componentContext: ComponentContext,
17+
@Assisted("fullPathOnFlipper") private val fullPathOnFlipper: Path,
18+
@Assisted private val onBack: DecomposeOnBackParameter,
19+
@Assisted private val onChanged: (Path) -> Unit,
20+
editFileNameViewModelFactory: EditFileNameViewModel.Factory
21+
) : ScreenDecomposeComponent(componentContext) {
22+
private val editFileNameViewModel = instanceKeeper.getOrCreate {
23+
editFileNameViewModelFactory.invoke(fullPathOnFlipper)
24+
}
25+
26+
@Composable
27+
override fun Render() {
28+
CreateFileDialogComposable(
29+
editFileNameViewModel = editFileNameViewModel,
30+
onFinish = { name ->
31+
fullPathOnFlipper.parent?.resolve(name)?.run(onChanged)
32+
},
33+
onDismiss = onBack::invoke
34+
)
35+
}
36+
37+
@AssistedFactory
38+
fun interface Factory {
39+
operator fun invoke(
40+
componentContext: ComponentContext,
41+
@Assisted("fullPathOnFlipper") fullPathOnFlipper: Path,
42+
onBack: DecomposeOnBackParameter,
43+
onChanged: (Path) -> Unit
44+
): EditFileNameDecomposeComponent
45+
}
46+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.flipperdevices.filemanager.editor.api
2+
3+
import androidx.compose.runtime.Composable
4+
import com.arkivanov.decompose.ComponentContext
5+
import com.arkivanov.decompose.extensions.compose.subscribeAsState
6+
import com.arkivanov.decompose.router.slot.SlotNavigation
7+
import com.arkivanov.decompose.router.slot.activate
8+
import com.arkivanov.decompose.router.slot.childSlot
9+
import com.arkivanov.decompose.router.slot.dismiss
10+
import com.arkivanov.essenty.instancekeeper.getOrCreate
11+
import com.flipperdevices.filemanager.editor.composable.FileManagerEditorComposable
12+
import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel
13+
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
14+
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent
15+
import dagger.assisted.Assisted
16+
import dagger.assisted.AssistedFactory
17+
import dagger.assisted.AssistedInject
18+
import kotlinx.serialization.Serializable
19+
import okio.Path
20+
21+
@Suppress("LongParameterList")
22+
class EditorDecomposeComponent @AssistedInject constructor(
23+
@Assisted componentContext: ComponentContext,
24+
@Assisted private val onBack: DecomposeOnBackParameter,
25+
@Assisted("fullPathOnFlipper") fullPathOnFlipper: Path,
26+
@Assisted("fullPathOnDevice") fullPathOnDevice: Path,
27+
@Assisted private val editFinishedCallback: EditFinishedCallback,
28+
editorViewModelFactory: EditorViewModel.Factory,
29+
editFileNameDecomposeComponentFactory: EditFileNameDecomposeComponent.Factory
30+
) : ScreenDecomposeComponent(componentContext) {
31+
32+
private val editorViewModel = instanceKeeper.getOrCreate {
33+
editorViewModelFactory.invoke(
34+
fullPathOnFlipper = fullPathOnFlipper,
35+
fullPathOnDevice = fullPathOnDevice
36+
)
37+
}
38+
39+
private fun saveFile() {
40+
editorViewModel.writeNow()
41+
editFinishedCallback.invoke(
42+
fullPathOnFlipper = editorViewModel.state.value.fullPathOnFlipper
43+
)
44+
}
45+
46+
@Serializable
47+
sealed interface SlotConfiguration {
48+
data object ChangeFlipperFileName : SlotConfiguration
49+
}
50+
51+
private val slotNavigation = SlotNavigation<SlotConfiguration>()
52+
53+
private val fileOptionsSlot = childSlot(
54+
source = slotNavigation,
55+
handleBackButton = true,
56+
serializer = SlotConfiguration.serializer(),
57+
childFactory = { config, childContext ->
58+
when (config) {
59+
SlotConfiguration.ChangeFlipperFileName -> {
60+
editFileNameDecomposeComponentFactory.invoke(
61+
componentContext = childContext,
62+
fullPathOnFlipper = editorViewModel.state.value.fullPathOnFlipper,
63+
onBack = slotNavigation::dismiss,
64+
onChanged = { fullPathOnFlipper ->
65+
editorViewModel.onFlipperPathChanged(fullPathOnFlipper)
66+
slotNavigation.dismiss()
67+
saveFile()
68+
}
69+
)
70+
}
71+
}
72+
}
73+
)
74+
75+
@Composable
76+
override fun Render() {
77+
FileManagerEditorComposable(
78+
editorViewModel = editorViewModel,
79+
onBack = onBack::invoke,
80+
onSaveAsClick = {
81+
slotNavigation.activate(SlotConfiguration.ChangeFlipperFileName)
82+
},
83+
onSaveClick = { saveFile() }
84+
)
85+
fileOptionsSlot.subscribeAsState().value.child?.instance?.Render()
86+
}
87+
88+
@AssistedFactory
89+
fun interface Factory {
90+
operator fun invoke(
91+
componentContext: ComponentContext,
92+
onBack: DecomposeOnBackParameter,
93+
@Assisted("fullPathOnFlipper") fullPathOnFlipper: Path,
94+
@Assisted("fullPathOnDevice") fullPathOnDevice: Path,
95+
editFinishedCallback: EditFinishedCallback
96+
): EditorDecomposeComponent
97+
}
98+
99+
fun interface EditFinishedCallback {
100+
fun invoke(fullPathOnFlipper: Path)
101+
}
102+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.flipperdevices.filemanager.editor.api
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.LaunchedEffect
8+
import androidx.compose.runtime.collectAsState
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.Color
12+
import com.arkivanov.decompose.ComponentContext
13+
import com.arkivanov.essenty.instancekeeper.getOrCreate
14+
import com.flipperdevices.filemanager.editor.composable.download.UploadingComposable
15+
import com.flipperdevices.filemanager.editor.viewmodel.DownloadViewModel
16+
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
17+
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent
18+
import dagger.assisted.Assisted
19+
import dagger.assisted.AssistedFactory
20+
import dagger.assisted.AssistedInject
21+
import flipperapp.components.filemngr.editor.impl.generated.resources.fme_status_downloading
22+
import kotlinx.coroutines.flow.filterIsInstance
23+
import kotlinx.coroutines.flow.launchIn
24+
import kotlinx.coroutines.flow.onEach
25+
import okio.Path
26+
import org.jetbrains.compose.resources.stringResource
27+
import flipperapp.components.filemngr.editor.impl.generated.resources.Res as FME
28+
29+
class FileDownloadDecomposeComponent @AssistedInject constructor(
30+
@Assisted componentContext: ComponentContext,
31+
@Assisted("fullPathOnFlipper") fullPathOnFlipper: Path,
32+
@Assisted("fullPathOnDevice") fullPathOnDevice: Path,
33+
@Assisted private val onDownloaded: () -> Unit,
34+
@Assisted private val onBack: DecomposeOnBackParameter,
35+
downloadViewModelFactory: DownloadViewModel.Factory
36+
) : ScreenDecomposeComponent(componentContext) {
37+
private val downloadViewModel = instanceKeeper.getOrCreate {
38+
downloadViewModelFactory.invoke(
39+
fullPathOnFlipper = fullPathOnFlipper,
40+
fullPathOnDevice = fullPathOnDevice
41+
)
42+
}
43+
44+
@Composable
45+
override fun Render() {
46+
LaunchedEffect(downloadViewModel) {
47+
downloadViewModel.state
48+
.filterIsInstance<DownloadViewModel.State.Downloaded>()
49+
.onEach {
50+
onDownloaded.invoke()
51+
}.launchIn(this)
52+
}
53+
val state by downloadViewModel.state.collectAsState()
54+
55+
when (val localState = state) {
56+
DownloadViewModel.State.CouldNotDownload -> {
57+
Box(Modifier.fillMaxSize().background(Color.Red))
58+
}
59+
60+
DownloadViewModel.State.Downloaded -> {
61+
Box(Modifier.fillMaxSize().background(Color.Green))
62+
}
63+
64+
is DownloadViewModel.State.Downloading -> {
65+
UploadingComposable(
66+
progress = localState.progress,
67+
fullPathOnFlipper = localState.fullPathOnFlipper,
68+
current = localState.downloaded,
69+
max = localState.total,
70+
speed = downloadViewModel.speedState.collectAsState().value,
71+
onCancel = onBack::invoke,
72+
modifier = Modifier,
73+
title = stringResource(FME.string.fme_status_downloading)
74+
)
75+
}
76+
77+
DownloadViewModel.State.TooLarge -> {
78+
Box(Modifier.fillMaxSize().background(Color.Cyan))
79+
}
80+
81+
DownloadViewModel.State.Unsupported -> {
82+
Box(Modifier.fillMaxSize().background(Color.Yellow))
83+
}
84+
}
85+
}
86+
87+
@AssistedFactory
88+
fun interface Factory {
89+
operator fun invoke(
90+
componentContext: ComponentContext,
91+
@Assisted("fullPathOnFlipper") fullPathOnFlipper: Path,
92+
@Assisted("fullPathOnDevice") fullPathOnDevice: Path,
93+
onBack: DecomposeOnBackParameter,
94+
onDownloaded: () -> Unit
95+
): FileDownloadDecomposeComponent
96+
}
97+
}

0 commit comments

Comments
 (0)