diff --git a/app/src/main/java/ua/acclorite/book_story/domain/browse/GroupedFiles.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/GroupedFiles.kt new file mode 100644 index 00000000..71ee6854 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/GroupedFiles.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt index cf042bdb..989b0433 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt @@ -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, + pinnedPaths: List, 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 ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt index 5fd39c67..3e0b2155 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt @@ -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, gridSize: Int, autoGridSize: Boolean, gridState: LazyGridState, - files: List, - hasSelectedItems: Boolean, - onLongItemClick: (SelectableFile) -> Unit, - onItemClick: (SelectableFile) -> Unit, + headerContent: @Composable (header: String, pinned: Boolean) -> Unit, + itemContent: @Composable (file: SelectableFile, files: List) -> 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 { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt index b56a16f9..70427dca 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseItem.kt @@ -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 ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt index dd6e4741..af6f048c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt @@ -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, - hasSelectedItems: Boolean, + pinnedPaths: List, 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) -> 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 ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayoutHeader.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayoutHeader.kt new file mode 100644 index 00000000..ebdb9447 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayoutHeader.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt index 07d03180..ce324fdd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListItem.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt index 2140af69..bab5c3c5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt @@ -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, - hasSelectedItems: Boolean, + groupedFiles: List, listState: LazyListState, - onLongItemClick: (SelectableFile) -> Unit, - onItemClick: (SelectableFile) -> Unit, + headerContent: @Composable (header: String, pinned: Boolean) -> Unit, + itemContent: @Composable (file: SelectableFile, files: List) -> 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 { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt index 9ec8385d..4acbf616 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt @@ -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, + pinnedPaths: List, 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 + ) + ) + } ) } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt index 1fac18b9..e6209a83 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt index 4e10b06f..d4539cb4 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt @@ -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) }, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt index 50d24cbb..01757894 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt index e2b6f787..f5d7cf3f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt @@ -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. */ diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt index 8c8b5e0d..452fdfab 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt @@ -112,6 +112,7 @@ data class MainState( val browseSortOrder: BrowseSortOrder = provideDefaultValue { BrowseSortOrder.LAST_MODIFIED }, val browseSortOrderDescending: Boolean = provideDefaultValue { true }, val browseIncludedFilterItems: List = provideDefaultValue { emptyList() }, + val browsePinnedPaths: List = provideDefaultValue { emptyList() }, ) : Parcelable { companion object { private fun 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 }, ) } } diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fe6be938..3f76cb59 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -463,4 +463,6 @@ Чекпоінт вперед Розділи Більше + Закріпити + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95c8af7c..dec1b1e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -623,5 +623,6 @@ Checkpoint forward Chapters More + Pin \ No newline at end of file