Skip to content

Commit

Permalink
File editor for new file manager (#979)
Browse files Browse the repository at this point in the history
**Background**

This PR adds file editing into experimental file manager

**Changes**

- Move part of upload module to KMP
- Extract file Uploader into decompose Composable content, which is now
shared
- Add file editing - with HEX/TXT
- Save into editable file, save as new file

**Test plan**

- Open bridge connection sample
- Create or find a file
- Click on file
- Write some text in hex/txt
- Save file
- See saved file changed
  • Loading branch information
makeevrserg authored Oct 30, 2024
1 parent 5c36520 commit 9f02e51
Show file tree
Hide file tree
Showing 56 changed files with 1,585 additions and 292 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Attention: don't forget to add the flag for F-Droid before release
- [Feature] Add illustrations, search, hold to dispatch into RemoteControls
- [Feature] Add metrics to infrared remotes
- [Feature] One-time open tools tab
- [Feature] Add file editing into new file manager
- [Refactor] Load RemoteControls from flipper, emulating animation
- [Refactor] Update to Kotlin 2.0
- [Refactor] Replace Ktorfit with Ktor requests in remote-controls
Expand Down
2 changes: 2 additions & 0 deletions components/bridge/connection/sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ dependencies {
implementation(projects.components.filemngr.upload.impl)
implementation(projects.components.filemngr.search.api)
implementation(projects.components.filemngr.search.impl)
implementation(projects.components.filemngr.editor.api)
implementation(projects.components.filemngr.editor.impl)

implementation(projects.components.newfilemanager.api)
implementation(projects.components.newfilemanager.impl)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.flipperdevices.filemanager.listing.impl.util
package com.flipperdevices.core.ktx.jre

/**
* Validates name for files which can be created on flippers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.flipperdevices.core.ui.ktx.image

import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.flipperdevices.core.ui.ktx.elements.FlipperProgressIndicator

@Composable
fun AndroidFlipperProgressIndicator(
accentColor: Color,
secondColor: Color,
@DrawableRes iconId: Int?,
percent: Float?,
modifier: Modifier = Modifier
) {
FlipperProgressIndicator(
accentColor = accentColor,
secondColor = secondColor,
painter = iconId?.let { painterResourceByKey(iconId) },
percent = percent,
modifier = modifier
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.flipperdevices.core.ui.ktx.elements

import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
Expand All @@ -19,9 +18,9 @@ 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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.flipperdevices.core.ui.ktx.image.painterResourceByKey
import com.flipperdevices.core.ui.theme.LocalPallet
import com.flipperdevices.core.ui.theme.LocalTypography
import kotlin.math.roundToInt
Expand All @@ -33,7 +32,7 @@ private const val PERCENT_MIN = 0.0001f
fun FlipperProgressIndicator(
accentColor: Color,
secondColor: Color,
@DrawableRes iconId: Int?,
painter: Painter?,
percent: Float?,
modifier: Modifier = Modifier
) {
Expand All @@ -49,12 +48,12 @@ fun FlipperProgressIndicator(
ComposableProgressRow(percent, accentColor)
}

if (iconId != null) {
if (painter != null) {
Icon(
modifier = Modifier
.padding(8.dp)
.size(28.dp),
painter = painterResourceByKey(iconId),
painter = painter,
contentDescription = null,
tint = LocalPallet.current.onFirmwareUpdateProgress
)
Expand Down
14 changes: 14 additions & 0 deletions components/filemngr/editor/api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugins {
id("flipper.multiplatform")
id("flipper.multiplatform-dependencies")
}

android.namespace = "com.flipperdevices.filemanager.editor.api"

commonDependencies {
implementation(projects.components.core.ui.decompose)

implementation(libs.compose.ui)
implementation(libs.decompose)
implementation(libs.okio)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.flipperdevices.filemanager.editor.api

import com.arkivanov.decompose.ComponentContext
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
import com.flipperdevices.ui.decompose.ScreenDecomposeComponent
import okio.Path

abstract class FileManagerEditorDecomposeComponent(
componentContext: ComponentContext
) : ScreenDecomposeComponent(componentContext) {
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
onBack: DecomposeOnBackParameter,
path: Path
): FileManagerEditorDecomposeComponent
}
}
66 changes: 66 additions & 0 deletions components/filemngr/editor/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
plugins {
id("flipper.multiplatform-compose")
id("flipper.multiplatform-dependencies")
id("flipper.anvil-multiplatform")
id("kotlinx-serialization")
}
android.namespace = "com.flipperdevices.filemanager.editor.impl"

commonDependencies {
implementation(projects.components.core.di)
implementation(projects.components.core.ktx)
implementation(projects.components.core.log)
implementation(projects.components.core.preference)
implementation(projects.components.core.storage)
implementation(projects.components.core.progress)
implementation(projects.components.core.ui.tabswitch)

implementation(projects.components.core.ui.lifecycle)
implementation(projects.components.core.ui.theme)
implementation(projects.components.core.ui.decompose)
implementation(projects.components.core.ui.ktx)
implementation(projects.components.core.ui.res)
implementation(projects.components.core.ui.dialog)
implementation(projects.components.core.ui.searchbar)

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.uiComponents)
implementation(projects.components.filemngr.editor.api)
implementation(projects.components.filemngr.upload.api)
implementation(projects.components.filemngr.main.api)

// Compose
implementation(libs.compose.ui)
implementation(libs.compose.tooling)
implementation(libs.compose.foundation)
implementation(libs.compose.material)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.core)
implementation(libs.compose.material.icons.extended)

implementation(libs.kotlin.serialization.json)
implementation(libs.ktor.client)

implementation(libs.decompose)
implementation(libs.kotlin.coroutines)
implementation(libs.essenty.lifecycle)
implementation(libs.essenty.lifecycle.coroutines)

implementation(libs.bundles.decompose)
implementation(libs.okio)
implementation(libs.kotlin.immutable.collections)
}

commonTestDependencies {
// Testing
implementation(libs.junit)
implementation(libs.ktx.testing)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.flipperdevices.filemanager.editor.composable

import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.flipperdevices.core.ui.theme.FlipperThemeInternal
import com.flipperdevices.filemanager.editor.composable.content.EditorLoadingContent
import com.flipperdevices.filemanager.editor.composable.content.ErrorContent
import com.flipperdevices.filemanager.editor.composable.content.TooBigContent

@Preview
@Composable
private fun ErrorContentPreview() {
FlipperThemeInternal {
Scaffold {
ErrorContent(modifier = Modifier.padding(it))
}
}
}

@Preview
@Composable
private fun LoadingContentPreview() {
FlipperThemeInternal {
Scaffold {
EditorLoadingContent(modifier = Modifier.padding(it))
}
}
}

@Preview
@Composable
private fun TooBigContentPreview() {
FlipperThemeInternal {
Scaffold {
TooBigContent(modifier = Modifier.padding(it))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.flipperdevices.filemanager.editor.composable

import android.annotation.SuppressLint
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.flipperdevices.core.ui.theme.FlipperThemeInternal
import com.flipperdevices.filemanager.editor.model.EditorEncodingEnum
import okio.Path.Companion.toPath

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Preview
@Composable
private fun EditorAppBarPreview() {
FlipperThemeInternal {
Scaffold(
topBar = {
EditorAppBar(
path = "file.txt".toPath(),
onBack = {},
onSaveClick = {},
onSaveAsClick = {},
editorEncodingEnum = EditorEncodingEnum.TEXT,
onEditorTabChange = {},
canSave = true
)
}
) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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_allowed_characters">Allowed characters: %1$s</string>
<string name="fme_txt">TXT</string>
<string name="fme_hex">HEX</string>
<string name="fme_save_as_dialog_title">Enter name:</string>
<string name="fme_save_as_dialog_button">Save as New File</string>
<string name="fme_save_as_dialog_chars">Allowed Characters: %1$s</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.flipperdevices.filemanager.editor.api

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ui.lifecycle.viewModelWithFactory
import com.flipperdevices.filemanager.editor.composable.FileManagerEditorComposable
import com.flipperdevices.filemanager.editor.composable.content.RenderLoadingScreen
import com.flipperdevices.filemanager.editor.composable.dialog.CreateFileDialogComposable
import com.flipperdevices.filemanager.editor.viewmodel.EditorViewModel
import com.flipperdevices.filemanager.editor.viewmodel.FileNameViewModel
import com.flipperdevices.filemanager.upload.api.UploaderDecomposeComponent
import com.flipperdevices.ui.decompose.DecomposeOnBackParameter
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.gulya.anvil.assisted.ContributesAssistedFactory
import okio.Path
import javax.inject.Provider

@ContributesAssistedFactory(AppGraph::class, FileManagerEditorDecomposeComponent.Factory::class)
class FileManagerEditorDecomposeComponentImpl @AssistedInject constructor(
@Assisted componentContext: ComponentContext,
@Assisted private val path: Path,
@Assisted private val onBack: DecomposeOnBackParameter,
private val editorViewModelFactory: EditorViewModel.Factory,
uploaderDecomposeComponentFactory: UploaderDecomposeComponent.Factory,
private val fileNameViewModelProvider: Provider<FileNameViewModel>,
) : FileManagerEditorDecomposeComponent(componentContext) {
private val uploaderDecomposeComponent = uploaderDecomposeComponentFactory.invoke(
componentContext = childContext("file_editor_$path")
)

@Composable
override fun Render() {
val fileNameViewModel = viewModelWithFactory(null) {
fileNameViewModelProvider.get()
}
val editorViewModel = viewModelWithFactory(path.toString()) {
editorViewModelFactory.invoke(path)
}

CreateFileDialogComposable(
fileNameViewModel = fileNameViewModel,
onFinish = onSaveClick@{ fileName ->
val rawContent = editorViewModel.getRawContent() ?: return@onSaveClick
uploaderDecomposeComponent.uploadRaw(
folderPath = path.parent ?: return@onSaveClick,
fileName = fileName,
content = rawContent
)
}
)

FileManagerEditorComposable(
path = path,
editorViewModel = editorViewModel,
uploaderDecomposeComponent = uploaderDecomposeComponent,
fileNameViewModel = fileNameViewModel,
onBack = onBack::invoke
)

uploaderDecomposeComponent.RenderLoadingScreen()
}
}
Loading

0 comments on commit 9f02e51

Please sign in to comment.