🚀 Pin paths in Browse

* Added replacement for directories structure
* Always show all files and paths to them
* Pin favorite paths

Resolves: #91
This commit is contained in:
Acclorite 2025-01-16 22:29:52 +02:00
parent 32cea59c7a
commit e49f010353
16 changed files with 204 additions and 79 deletions

View file

@ -0,0 +1,10 @@
package ua.acclorite.book_story.domain.browse
import androidx.compose.runtime.Immutable
@Immutable
data class GroupedFiles(
val header: String,
val pinned: Boolean,
val files: List<SelectableFile>
)

View file

@ -14,6 +14,7 @@ import ua.acclorite.book_story.domain.library.book.SelectableNullableBook
import ua.acclorite.book_story.domain.util.BottomSheet
import ua.acclorite.book_story.domain.util.Dialog
import ua.acclorite.book_story.ui.browse.BrowseEvent
import ua.acclorite.book_story.ui.main.MainEvent
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
@Composable
@ -31,6 +32,7 @@ fun BrowseContent(
gridSize: Int,
autoGridSize: Boolean,
includedFilterItems: List<String>,
pinnedPaths: List<String>,
canScrollBackList: Boolean,
canScrollBackGrid: Boolean,
hasSelectedItems: Boolean,
@ -59,6 +61,7 @@ fun BrowseContent(
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
changePinnedPaths: (MainEvent.OnChangeBrowsePinnedPaths) -> Unit,
navigateToLibrary: () -> Unit,
navigateToHelp: () -> Unit,
) {
@ -89,6 +92,7 @@ fun BrowseContent(
gridSize = gridSize,
autoGridSize = autoGridSize,
includedFilterItems = includedFilterItems,
pinnedPaths = pinnedPaths,
canScrollBackList = canScrollBackList,
canScrollBackGrid = canScrollBackGrid,
hasSelectedItems = hasSelectedItems,
@ -112,6 +116,7 @@ fun BrowseContent(
selectFile = selectFile,
showFilterBottomSheet = showFilterBottomSheet,
showAddDialog = showAddDialog,
changePinnedPaths = changePinnedPaths,
navigateToHelp = navigateToHelp
)

View file

@ -1,5 +1,6 @@
package ua.acclorite.book_story.presentation.browse
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -10,7 +11,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.browse.BrowseLayout
import ua.acclorite.book_story.domain.browse.GroupedFiles
import ua.acclorite.book_story.domain.browse.SelectableFile
import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar
import ua.acclorite.book_story.presentation.core.components.common.header
@ -19,43 +20,34 @@ import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollb
@Composable
fun BrowseGridLayout(
groupedFiles: List<GroupedFiles>,
gridSize: Int,
autoGridSize: Boolean,
gridState: LazyGridState,
files: List<SelectableFile>,
hasSelectedItems: Boolean,
onLongItemClick: (SelectableFile) -> Unit,
onItemClick: (SelectableFile) -> Unit,
headerContent: @Composable (header: String, pinned: Boolean) -> Unit,
itemContent: @Composable (file: SelectableFile, files: List<SelectableFile>) -> Unit
) {
LazyVerticalGridWithScrollbar(
columns = if (autoGridSize) GridCells.Adaptive(170.dp)
else GridCells.Fixed(gridSize.coerceAtLeast(1)),
state = gridState,
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 8.dp),
scrollbarSettings = Constants.providePrimaryScrollbar(false)
) {
header {
Spacer(modifier = Modifier.height(8.dp))
}
groupedFiles.forEach { group ->
stickyHeader {
headerContent(group.header, group.pinned)
}
items(
files,
key = { it.path }
) { selectableFile ->
BrowseItem(
file = selectableFile,
modifier = Modifier.animateItem(),
layout = BrowseLayout.GRID,
hasSelectedItems = hasSelectedItems,
onLongClick = {
onLongItemClick(selectableFile)
},
onClick = {
onItemClick(selectableFile)
items(
group.files,
key = { it.path }
) { selectableFile ->
Box(Modifier.animateItem()) {
itemContent(selectableFile, group.files)
}
)
}
}
header {

View file

@ -7,10 +7,10 @@ import ua.acclorite.book_story.domain.browse.SelectableFile
@Composable
fun BrowseItem(
modifier: Modifier = Modifier,
layout: BrowseLayout,
file: SelectableFile,
hasSelectedItems: Boolean,
modifier: Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit
) {

View file

@ -3,41 +3,56 @@ package ua.acclorite.book_story.presentation.browse
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import ua.acclorite.book_story.domain.browse.BrowseLayout
import ua.acclorite.book_story.domain.browse.GroupedFiles
import ua.acclorite.book_story.domain.browse.SelectableFile
import java.io.File
@Composable
fun BrowseLayout(
files: List<SelectableFile>,
hasSelectedItems: Boolean,
pinnedPaths: List<String>,
layout: BrowseLayout,
gridSize: Int,
autoGridSize: Boolean,
listState: LazyListState,
gridState: LazyGridState,
onLongItemClick: (SelectableFile) -> Unit,
onItemClick: (SelectableFile) -> Unit
headerContent: @Composable (header: String, pinned: Boolean) -> Unit,
itemContent: @Composable (file: SelectableFile, files: List<SelectableFile>) -> Unit
) {
val groupedFiles = remember(files, pinnedPaths) {
files.groupBy { file ->
file.path.substringBeforeLast(File.separator)
}.map { (header, files) ->
GroupedFiles(
header = header,
pinned = pinnedPaths.any { it.lowercase().trim() == header.lowercase().trim() },
files = files
)
}.sortedByDescending { group ->
group.pinned
}
}
when (layout) {
BrowseLayout.LIST -> {
BrowseListLayout(
files = files,
hasSelectedItems = hasSelectedItems,
groupedFiles = groupedFiles,
listState = listState,
onLongItemClick = onLongItemClick,
onItemClick = onItemClick
headerContent = headerContent,
itemContent = itemContent
)
}
BrowseLayout.GRID -> {
BrowseGridLayout(
groupedFiles = groupedFiles,
gridSize = gridSize,
autoGridSize = autoGridSize,
files = files,
hasSelectedItems = hasSelectedItems,
gridState = gridState,
onLongItemClick = onLongItemClick,
onItemClick = onItemClick
headerContent = headerContent,
itemContent = itemContent
)
}
}

View file

@ -0,0 +1,56 @@
package ua.acclorite.book_story.presentation.browse
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
@Composable
fun BrowseLayoutHeader(
header: String,
pinned: Boolean,
pin: () -> Unit
) {
Row(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 8.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
modifier = Modifier.weight(1f),
text = header,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge
)
Icon(
modifier = Modifier
.size(20.dp)
.noRippleClickable {
pin()
},
imageVector = if (pinned) Icons.Default.PushPin
else Icons.Outlined.PushPin,
contentDescription = stringResource(R.string.pin_content_desc),
tint = if (pinned) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View file

@ -33,7 +33,7 @@ fun BrowseListItem(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 3.dp)
.padding(vertical = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(
if (file.selected) MaterialTheme.colorScheme.secondaryContainer

View file

@ -1,5 +1,7 @@
package ua.acclorite.book_story.presentation.browse
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
@ -8,7 +10,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.browse.BrowseLayout
import ua.acclorite.book_story.domain.browse.GroupedFiles
import ua.acclorite.book_story.domain.browse.SelectableFile
import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar
import ua.acclorite.book_story.presentation.core.constants.Constants
@ -16,37 +18,30 @@ import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollb
@Composable
fun BrowseListLayout(
files: List<SelectableFile>,
hasSelectedItems: Boolean,
groupedFiles: List<GroupedFiles>,
listState: LazyListState,
onLongItemClick: (SelectableFile) -> Unit,
onItemClick: (SelectableFile) -> Unit,
headerContent: @Composable (header: String, pinned: Boolean) -> Unit,
itemContent: @Composable (file: SelectableFile, files: List<SelectableFile>) -> Unit
) {
LazyColumnWithScrollbar(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 8.dp),
scrollbarSettings = Constants.providePrimaryScrollbar(false)
) {
item {
Spacer(modifier = Modifier.height(8.dp))
}
groupedFiles.forEach { group ->
stickyHeader {
headerContent(group.header, group.pinned)
}
items(
files,
key = { it.path }
) { selectableFile ->
BrowseItem(
file = selectableFile,
layout = BrowseLayout.LIST,
modifier = Modifier.animateItem(),
hasSelectedItems = hasSelectedItems,
onLongClick = {
onLongItemClick(selectableFile)
},
onClick = {
onItemClick(selectableFile)
items(
group.files,
key = { it.path }
) { selectableFile ->
Box(Modifier.animateItem()) {
itemContent(selectableFile, group.files)
}
)
}
}
item {

View file

@ -15,12 +15,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.browse.BrowseLayout
import ua.acclorite.book_story.domain.browse.SelectableFile
import ua.acclorite.book_story.presentation.core.util.LocalActivity
import ua.acclorite.book_story.presentation.core.util.showToast
import ua.acclorite.book_story.ui.browse.BrowseEvent
import ua.acclorite.book_story.ui.main.MainEvent
import ua.acclorite.book_story.ui.theme.DefaultTransition
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
@ -35,6 +33,7 @@ fun BrowseScaffold(
gridSize: Int,
autoGridSize: Boolean,
includedFilterItems: List<String>,
pinnedPaths: List<String>,
canScrollBackList: Boolean,
canScrollBackGrid: Boolean,
hasSelectedItems: Boolean,
@ -57,10 +56,9 @@ fun BrowseScaffold(
permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit,
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
changePinnedPaths: (MainEvent.OnChangeBrowsePinnedPaths) -> Unit,
navigateToHelp: () -> Unit,
) {
val context = LocalActivity.current
Scaffold(
modifier = Modifier
.fillMaxSize()
@ -97,24 +95,46 @@ fun BrowseScaffold(
DefaultTransition(visible = !isLoading) {
BrowseLayout(
files = files,
pinnedPaths = pinnedPaths,
layout = layout,
hasSelectedItems = hasSelectedItems,
gridSize = gridSize,
autoGridSize = autoGridSize,
listState = listState,
gridState = gridState,
onLongItemClick = { file ->
context.getString(
R.string.file_path_query,
file.path
).showToast(context = context)
headerContent = { header, pinned ->
BrowseLayoutHeader(
header = header,
pinned = pinned,
pin = {
changePinnedPaths(
MainEvent.OnChangeBrowsePinnedPaths(
value = header
)
)
}
)
},
onItemClick = { file ->
selectFile(
BrowseEvent.OnSelectFile(
includedFileFormats = includedFilterItems,
file = file
)
itemContent = { file, groupFiles ->
BrowseItem(
file = file,
layout = layout,
hasSelectedItems = hasSelectedItems,
onLongClick = {
selectFiles(
BrowseEvent.OnSelectFiles(
includedFileFormats = includedFilterItems,
files = groupFiles
)
)
},
onClick = {
selectFile(
BrowseEvent.OnSelectFile(
includedFileFormats = includedFilterItems,
file = file
)
)
}
)
}
)

View file

@ -65,4 +65,5 @@ object DataStoreConstants {
val BROWSE_SORT_ORDER = stringPreferencesKey("browse_sort_order")
val BROWSE_SORT_ORDER_DESCENDING = booleanPreferencesKey("browse_sort_order_descending")
val BROWSE_INCLUDED_FILTER_ITEMS = stringSetPreferencesKey("browse_included_filter_items")
val BROWSE_PINNED_PATHS = stringSetPreferencesKey("browse_pinned_paths")
}

View file

@ -153,6 +153,7 @@ object BrowseScreen : Screen, Parcelable {
gridSize = mainState.value.browseGridSize,
autoGridSize = mainState.value.browseAutoGridSize,
includedFilterItems = mainState.value.browseIncludedFilterItems,
pinnedPaths = mainState.value.browsePinnedPaths,
canScrollBackList = listState.canScrollBackward,
canScrollBackGrid = gridState.canScrollBackward,
hasSelectedItems = state.value.hasSelectedItems,
@ -181,6 +182,7 @@ object BrowseScreen : Screen, Parcelable {
dismissAddDialog = screenModel::onEvent,
selectAddDialog = screenModel::onEvent,
actionAddDialog = screenModel::onEvent,
changePinnedPaths = mainModel::onEvent,
navigateToLibrary = {
navigator.push(LibraryScreen, saveInBackStack = false)
},

View file

@ -57,4 +57,5 @@ sealed class MainEvent {
data class OnChangeProgressBarPadding(val value: Int) : MainEvent()
data class OnChangeProgressBarAlignment(val value: String) : MainEvent()
data class OnChangeProgressBarFontSize(val value: Int) : MainEvent()
data class OnChangeBrowsePinnedPaths(val value: String) : MainEvent()
}

View file

@ -229,7 +229,7 @@ class MainModel @Inject constructor(
)
is MainEvent.OnChangeBrowseIncludedFilterItem -> handleBrowseIncludedFilterItemUpdate(
event
event = event
)
is MainEvent.OnChangeTextAlignment -> handleDatastoreUpdate(
@ -479,6 +479,10 @@ class MainModel @Inject constructor(
it.copy(progressBarFontSize = this)
}
)
is MainEvent.OnChangeBrowsePinnedPaths -> handleBrowsePinnedPathsUpdate(
event = event
)
}
}
@ -546,6 +550,22 @@ class MainModel @Inject constructor(
)
}
private fun handleBrowsePinnedPathsUpdate(
event: MainEvent.OnChangeBrowsePinnedPaths
) {
val set = _state.value.browsePinnedPaths.toMutableSet()
if (!set.add(event.value)) {
set.remove(event.value)
}
handleDatastoreUpdate(
key = DataStoreConstants.BROWSE_PINNED_PATHS,
value = set,
updateState = {
it.copy(browsePinnedPaths = toList())
}
)
}
/**
* Handles and updates Datastore.
*/

View file

@ -112,6 +112,7 @@ data class MainState(
val browseSortOrder: BrowseSortOrder = provideDefaultValue { BrowseSortOrder.LAST_MODIFIED },
val browseSortOrderDescending: Boolean = provideDefaultValue { true },
val browseIncludedFilterItems: List<String> = provideDefaultValue { emptyList() },
val browsePinnedPaths: List<String> = provideDefaultValue { emptyList() },
) : Parcelable {
companion object {
private fun <D> provideDefaultValue(calculation: () -> D): D {
@ -345,6 +346,10 @@ data class MainState(
progressBarFontSize = provideValue(
PROGRESS_BAR_FONT_SIZE
) { progressBarFontSize },
browsePinnedPaths = provideValue(
BROWSE_PINNED_PATHS, convert = { toList() }
) { browsePinnedPaths },
)
}
}

View file

@ -463,4 +463,6 @@
<string name="checkpoint_forward_content_desc">Чекпоінт вперед</string>
<string name="chapters_content_desc">Розділи</string>
<string name="more_content_desc">Більше</string>
<string name="pin_content_desc">Закріпити</string>
</resources>

View file

@ -623,5 +623,6 @@
<string name="checkpoint_forward_content_desc">Checkpoint forward</string>
<string name="chapters_content_desc">Chapters</string>
<string name="more_content_desc">More</string>
<string name="pin_content_desc">Pin</string>
</resources>