Skip to content

Commit

Permalink
add subfolders count to file manager (#991)
Browse files Browse the repository at this point in the history
**Background**

Current file manager doesn't display subitems count inside folders

**Changes**

- Add subitems count
- Add `ExtendedListingItem` for better scalability 
- Add `visible` parameter for PlaceholderConnecting so there will be
animation when it's loaded
 
**Test plan**

- Open sample app
- Open folders and see items are loading and each after each without
blocking the listing itself
  • Loading branch information
makeevrserg authored Nov 26, 2024
1 parent bcebc1c commit 895a2f6
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [FIX] Fix empty response in faphub category
- [FIX] New file manager uploading progress
- [FIX] Fix build when no metrics enabled
- [Feature] Add count subfolders for new file manager

# 1.8.0
Attention: don't forget to add the flag for F-Droid before release
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import io.github.fornewid.placeholder.foundation.placeholder
import io.github.fornewid.placeholder.foundation.shimmer

@Suppress("ModifierComposed") // MOB-1039
fun Modifier.placeholderConnecting(shape: Int = 4) = composed {
fun Modifier.placeholderConnecting(
shape: Int = 4,
visible: Boolean = true
) = composed {
this.then(
placeholder(
visible = true,
visible = visible,
shape = RoundedCornerShape(shape.dp),
color = LocalPallet.current.placeholder.copy(alpha = 0.2f),
highlight = PlaceholderHighlight.shimmer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

<string name="fml_appbar_title">File Manager</string>

<string name="fml_items_in_folder">%1$s items</string>
<string name="fml_no_files">No Files Yet</string>
<string name="fml_upload_files">Upload Files</string>
<string name="fml_selection_select_all">Select All</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType
import com.flipperdevices.core.ktx.jre.toFormattedSize
import com.flipperdevices.core.preference.pb.FileManagerOrientation
import com.flipperdevices.filemanager.listing.impl.model.ExtendedListingItem
import com.flipperdevices.filemanager.listing.impl.model.PathWithType
import com.flipperdevices.filemanager.listing.impl.viewmodel.DeleteFilesViewModel
import com.flipperdevices.filemanager.listing.impl.viewmodel.FilesViewModel
Expand All @@ -19,9 +20,12 @@ import com.flipperdevices.filemanager.ui.components.itemcard.FolderCardPlacehold
import com.flipperdevices.filemanager.ui.components.itemcard.components.asPainter
import com.flipperdevices.filemanager.ui.components.itemcard.components.asTint
import com.flipperdevices.filemanager.ui.components.itemcard.model.ItemUiSelectionState
import flipperapp.components.filemngr.listing.impl.generated.resources.fml_items_in_folder
import okio.Path
import org.jetbrains.compose.resources.stringResource
import flipperapp.components.filemngr.listing.impl.generated.resources.Res as FML

@Suppress("FunctionNaming", "LongParameterList")
@Suppress("FunctionNaming", "LongParameterList", "LongMethod")
fun LazyGridScope.LoadedFilesComposable(
path: Path,
deleteFileState: DeleteFilesViewModel.State,
Expand All @@ -37,7 +41,7 @@ fun LazyGridScope.LoadedFilesComposable(
) {
items(filesState.files) { file ->
val isFileLoading = remember(deleteFileState.fileNamesOrNull) {
deleteFileState.fileNamesOrNull.orEmpty().contains(file.fileName)
deleteFileState.fileNamesOrNull.orEmpty().contains(file.itemName)
}
Crossfade(isFileLoading) { animatedIsFileLoading ->
if (animatedIsFileLoading) {
Expand All @@ -49,41 +53,49 @@ fun LazyGridScope.LoadedFilesComposable(
orientation = orientation,
)
} else {
val filePathWithType = remember(path, file.fileName) {
val fullPath = path.resolve(file.fileName)
PathWithType(file.fileType ?: FileType.FILE, fullPath)
val filePathWithType = remember(path, file.itemName) {
val fullPath = path.resolve(file.itemName)
PathWithType(file.itemType, fullPath)
}
FolderCardComposable(
modifier = Modifier
.fillMaxWidth()
.animateItem()
.animateContentSize(),
painter = file.asPainter(),
iconTint = file.asTint(),
title = file.fileName,
painter = file.asListingItem().asPainter(),
iconTint = file.asListingItem().asTint(),
title = file.itemName,
canDeleteFiles = canDeleteFiles,
subtitle = file.size.toFormattedSize(),
subtitle = when (file) {
is ExtendedListingItem.File -> file.size.toFormattedSize()
is ExtendedListingItem.Folder -> stringResource(
resource = FML.string.fml_items_in_folder,
file.itemsCount ?: 0
)
},
isSubtitleLoading = when (file) {
is ExtendedListingItem.File -> false
is ExtendedListingItem.Folder -> file.itemsCount == null
},
selectionState = when {
selectionState.selected.contains(filePathWithType) -> ItemUiSelectionState.SELECTED
selectionState.isEnabled -> ItemUiSelectionState.UNSELECTED
else -> ItemUiSelectionState.NONE
},
onClick = {
when (file.fileType) {
when (file.itemType) {
FileType.DIR -> {
onPathChanged.invoke(filePathWithType.fullPath)
}

FileType.FILE -> {
onEditFileClick(filePathWithType.fullPath)
}

null -> Unit
}
},
onCheckChange = { onCheckToggle.invoke(filePathWithType) },
onMoreClick = { onFileMoreClick.invoke(filePathWithType) },
onDelete = { onDelete.invoke(path.resolve(file.fileName)) },
onDelete = { onDelete.invoke(path.resolve(file.itemName)) },
orientation = orientation
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ fun FileListAppBar(
.orEmpty()
.map {
PathWithType(
fileType = it.fileType ?: FileType.FILE,
fullPath = path.resolve(it.fileName)
fileType = it.itemType,
fullPath = path.resolve(it.itemName)
)
}
selectionViewModel.select(paths)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.flipperdevices.filemanager.listing.impl.model

import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType
import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem
import okio.Path

sealed interface ExtendedListingItem {
/**
* Local file-only path
* example: file.txt, item.svg
*/
val path: Path

val itemType: FileType

val itemName: String
get() = path.name

fun asListingItem() = ListingItem(
fileName = itemName,
fileType = itemType,
size = (this as? File)?.size ?: 0
)

/**
* @param path file name path. Not full path
* @param size file size in bytes
*/
data class File(
override val path: Path,
val size: Long
) : ExtendedListingItem {
override val itemType: FileType = FileType.FILE
}

/**
* @param path file name path. Not full path
* @param itemsCount amount of items inside
*/
data class Folder(
override val path: Path,
val itemsCount: Int? = null,
) : ExtendedListingItem {
override val itemType: FileType = FileType.DIR
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import com.flipperdevices.bridge.connection.feature.provider.api.get
import com.flipperdevices.bridge.connection.feature.provider.api.getSync
import com.flipperdevices.bridge.connection.feature.storage.api.FStorageFeatureApi
import com.flipperdevices.bridge.connection.feature.storage.api.fm.FListingStorageApi
import com.flipperdevices.bridge.connection.feature.storage.api.model.FileType
import com.flipperdevices.bridge.connection.feature.storage.api.model.ListingItem
import com.flipperdevices.core.ktx.jre.launchWithLock
import com.flipperdevices.core.ktx.jre.toThrowableFlow
import com.flipperdevices.core.ktx.jre.withLock
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.core.preference.pb.FileManagerSort
import com.flipperdevices.core.preference.pb.Settings
import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel
import com.flipperdevices.filemanager.listing.impl.model.ExtendedListingItem
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
Expand All @@ -24,12 +27,17 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.sync.Mutex
import okio.Path
import okio.Path.Companion.toPath

class FilesViewModel @AssistedInject constructor(
private val featureProvider: FFeatureProvider,
Expand All @@ -56,15 +64,22 @@ class FilesViewModel @AssistedInject constructor(
if (settings.show_hidden_files_on_flipper) {
true
} else {
!it.fileName.startsWith(".")
!it.path.name.startsWith(".")
}
}
.sortedByDescending {
when (settings.file_manager_sort) {
is FileManagerSort.Unrecognized,
FileManagerSort.DEFAULT -> null

FileManagerSort.SIZE -> it.size
FileManagerSort.SIZE -> {
when (it) {
is ExtendedListingItem.File -> it.size
// The default size for folder is 0
// Here's placed 0 so sort works as on flipper
is ExtendedListingItem.Folder -> 0
}
}
}
}
.toImmutableList()
Expand All @@ -74,12 +89,62 @@ class FilesViewModel @AssistedInject constructor(
}
).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading)

private suspend fun updateFiles(
items: List<ExtendedListingItem>,
listingApi: FListingStorageApi
) {
items
.filterIsInstance<ExtendedListingItem.Folder>()
.filter { directory -> directory.itemsCount == null }
.onEach { directory ->
_state.update { state ->
val loadedState = (state as? State.Loaded)
if (loadedState == null) {
error { "#updateFiles state changed during update" }
return@update state
}
val newList = loadedState.files.toMutableList()
val i = newList.indexOfFirst { item -> item == directory }
if (i == -1) {
error { "#updateFiles could not find item in list" }
return@update loadedState
}
val itemsCount = listingApi.ls(path.resolve(directory.path).toString())
.getOrNull()
.orEmpty()
.size
val updatedDirectory = directory.copy(itemsCount = itemsCount)
newList[i] = updatedDirectory
loadedState.copy(files = newList.toImmutableList())
}
}
}

private fun ListingItem.toExtended(): ExtendedListingItem {
return when (fileType) {
FileType.DIR -> {
ExtendedListingItem.Folder(
path = fileName.toPath(),
itemsCount = null
)
}

null, FileType.FILE -> {
ExtendedListingItem.File(
path = fileName.toPath(),
size = size
)
}
}
}

private suspend fun listFiles(listingApi: FListingStorageApi) {
listingApi.lsFlow(path.toString())
.toThrowableFlow()
.catch { _state.emit(State.CouldNotListPath) }
.map { items -> items.map { item -> item.toExtended() } }
.onEach { files ->
_state.update { state ->
_state.updateAndGet { state ->
when (state) {
is State.Loaded -> {
state.copy(state.files.plus(files).toImmutableList())
Expand All @@ -97,7 +162,7 @@ class FilesViewModel @AssistedInject constructor(
val loadedState = _state.value as? State.Loaded ?: return
_state.update {
val newFileList = loadedState.files
.filter { it.fileName != path.name }
.filter { it.path.name != path.name }
.toImmutableList()
loadedState.copy(files = newFileList)
}
Expand All @@ -120,8 +185,8 @@ class FilesViewModel @AssistedInject constructor(
(state as? State.Loaded)?.let { loadedState ->
val newItemsNames = items.map(ListingItem::fileName)
val newFiles = loadedState.files
.filter { item -> !newItemsNames.contains(item.fileName) }
.plus(items)
.filter { item -> !newItemsNames.contains(item.itemName) }
.plus(items.map { item -> item.toExtended() })
.toImmutableList()
loadedState.copy(files = newFiles)
} ?: state
Expand Down Expand Up @@ -152,14 +217,28 @@ class FilesViewModel @AssistedInject constructor(
.get<FStorageFeatureApi>()
.onEach { featureStatus -> invalidate(featureStatus) }
.launchIn(viewModelScope)
combine(
flow = featureProvider
.get<FStorageFeatureApi>()
.filterIsInstance<FFeatureStatus.Supported<FStorageFeatureApi>>(),
flow2 = state
.filterIsInstance<State.Loaded>()
.distinctUntilChangedBy { it.files.size },
transform = { feature, state ->
updateFiles(
items = state.files,
listingApi = feature.featureApi.listingApi()
)
}
).launchIn(viewModelScope)
}

sealed interface State {
data object Loading : State
data object Unsupported : State
data object CouldNotListPath : State
data class Loaded(
val files: ImmutableList<ListingItem>,
val files: ImmutableList<ExtendedListingItem>,
) : State
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fun LazyListScope.FolderCardListLazyComposable(
subtitle = file.fullPath.parent
?.toString()
?: file.instance.size.toFormattedSize(),
isSubtitleLoading = false,
selectionState = ItemUiSelectionState.NONE,
onClick = {
when (file.instance.fileType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ private fun FolderCardGridComposablePreview() {
selectionState = selectionState,
onClick = {},
onCheckChange = {},
onMoreClick = {}
onMoreClick = {},
isSubtitleLoading = false
)
}
ItemUiSelectionState.entries.forEach { selectionState ->
Expand All @@ -35,7 +36,20 @@ private fun FolderCardGridComposablePreview() {
selectionState = selectionState,
onClick = {},
onCheckChange = {},
onMoreClick = {}
onMoreClick = {},
isSubtitleLoading = false
)
}
ItemUiSelectionState.entries.forEach { selectionState ->
FolderCardGridComposable(
painter = painterResource(FR.drawable.ic_folder_black),
title = "A very very ultra mega super duper log title with some message at the end",
subtitle = "A very very ultra mega super duper log title with some message at the end",
selectionState = selectionState,
onClick = {},
onCheckChange = {},
onMoreClick = {},
isSubtitleLoading = true
)
}
}
Expand Down
Loading

0 comments on commit 895a2f6

Please sign in to comment.