LibraryScreen: New LibraryTopBar + Moved LaunchedEffect in LibraryScreenRoot.

This commit is contained in:
acclorite 2024-06-24 18:09:36 +03:00
parent d159b2e902
commit 3967cf787d
2 changed files with 214 additions and 169 deletions

View file

@ -1,3 +1,5 @@
@file:OptIn(ExperimentalMaterialApi::class)
package ua.acclorite.book_story.presentation.screens.library package ua.acclorite.book_story.presentation.screens.library
import android.widget.Toast import android.widget.Toast
@ -5,37 +7,25 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
@ -47,15 +37,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,11 +49,7 @@ import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.OnNavigate import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.MoreDropDown
import ua.acclorite.book_story.presentation.components.customItems import ua.acclorite.book_story.presentation.components.customItems
import ua.acclorite.book_story.presentation.components.header import ua.acclorite.book_story.presentation.components.header
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
@ -79,7 +60,7 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem import ua.acclorite.book_story.presentation.screens.library.components.LibraryBookItem
import ua.acclorite.book_story.presentation.screens.library.components.LibraryTabRow import ua.acclorite.book_story.presentation.screens.library.components.LibraryTopBar
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryDeleteDialog
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
@ -97,48 +78,49 @@ fun LibraryScreenRoot() {
val state = viewModel.state.collectAsState() val state = viewModel.state.collectAsState()
val pagerState = rememberPagerState(
initialPage = state.value.currentPage,
pageCount = { Category.entries.size }
)
val refreshState = rememberPullRefreshState(
refreshing = state.value.isRefreshing,
onRefresh = {
viewModel.onEvent(LibraryEvent.OnRefreshList)
}
)
LaunchedEffect(Unit) {
viewModel.onEvent(LibraryEvent.OnScrollToPage(state.value.currentPage, pagerState))
}
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.value.currentPage) {
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
}
}
LibraryScreen( LibraryScreen(
state = state, state = state,
onNavigate = { navigator.it() }, onNavigate = { navigator.it() },
pagerState = pagerState,
refreshState = refreshState,
onEvent = viewModel::onEvent, onEvent = viewModel::onEvent,
onBrowseEvent = browseViewModel::onEvent, onBrowseEvent = browseViewModel::onEvent,
onHistoryEvent = historyViewModel::onEvent onHistoryEvent = historyViewModel::onEvent
) )
} }
@OptIn( @OptIn(ExperimentalMaterialApi::class)
ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class
)
@Composable @Composable
private fun LibraryScreen( private fun LibraryScreen(
state: State<LibraryState>, state: State<LibraryState>,
onNavigate: OnNavigate, onNavigate: OnNavigate,
pagerState: PagerState,
refreshState: PullRefreshState,
onEvent: (LibraryEvent) -> Unit, onEvent: (LibraryEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit, onHistoryEvent: (HistoryEvent) -> Unit,
onBrowseEvent: (BrowseEvent) -> Unit onBrowseEvent: (BrowseEvent) -> Unit
) { ) {
val pagerState = rememberPagerState(
initialPage = state.value.currentPage,
pageCount = { Category.entries.size }
)
var isScrollInProgress by remember { mutableStateOf(false) } var isScrollInProgress by remember { mutableStateOf(false) }
val refreshState = rememberPullRefreshState(
refreshing = state.value.isRefreshing,
onRefresh = {
onEvent(LibraryEvent.OnRefreshList)
}
)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
onEvent(LibraryEvent.OnScrollToPage(state.value.currentPage, pagerState))
}
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != state.value.currentPage) {
onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
}
}
if (state.value.showMoveDialog) { if (state.value.showMoveDialog) {
LibraryMoveDialog( LibraryMoveDialog(
@ -163,127 +145,11 @@ private fun LibraryScreen(
.pullRefresh(refreshState), .pullRefresh(refreshState),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
Column(Modifier.fillMaxWidth()) { LibraryTopBar(
AnimatedTopAppBar( state = state,
scrollBehavior = null, pagerState = pagerState,
isTopBarScrolled = state.value.hasSelectedItems, onEvent = onEvent
content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
content1NavigationIcon = {},
content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(id = R.string.library_screen))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = state.value.books.size.toString(),
modifier = Modifier
.background(
MaterialTheme.colorScheme.surfaceContainer,
RoundedCornerShape(14.dp)
) )
.padding(horizontal = 8.dp, vertical = 3.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 16.sp
)
}
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Search,
contentDescription = R.string.search_content_desc,
disableOnClick = true,
) {
onEvent(LibraryEvent.OnSearchShowHide)
}
MoreDropDown()
},
content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = {
CustomIconButton(
icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true
) {
onEvent(LibraryEvent.OnClearSelectedBooks)
}
},
content2Title = {
Text(
stringResource(
id = R.string.selected_items_count_query,
state.value.selectedItemsCount.coerceAtLeast(1)
),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
},
content2Actions = {
CustomIconButton(
icon = Icons.AutoMirrored.Outlined.DriveFileMove,
contentDescription = R.string.move_books_content_desc,
enabled = !state.value.isLoading
&& !state.value.isRefreshing
&& !state.value.showMoveDialog,
disableOnClick = false,
) {
onEvent(LibraryEvent.OnShowHideMoveDialog)
}
CustomIconButton(
icon = Icons.Outlined.Delete,
contentDescription = R.string.delete_books_content_desc,
enabled = !state.value.isLoading
&& !state.value.isRefreshing
&& !state.value.showDeleteDialog,
disableOnClick = false
) {
onEvent(LibraryEvent.OnShowHideDeleteDialog)
}
},
content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
content3NavigationIcon = {
CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc,
disableOnClick = true
) {
onEvent(
LibraryEvent.OnSearchShowHide
)
}
},
content3Title = {
CustomSearchTextField(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
onEvent(LibraryEvent.OnRequestFocus(focusRequester))
},
query = state.value.searchQuery,
onQueryChange = {
onEvent(LibraryEvent.OnSearchQueryChange(it))
},
onSearch = {
onEvent(LibraryEvent.OnSearch)
},
placeholder = stringResource(
id = R.string.search_query,
stringResource(id = R.string.books)
)
)
},
content3Actions = {
MoreDropDown()
}
)
LibraryTabRow(
onEvent = onEvent,
books = state.value.categorizedBooks,
pagerState = pagerState
)
}
} }
) { paddingValues -> ) { paddingValues ->
Box( Box(

View file

@ -0,0 +1,179 @@
package ua.acclorite.book_story.presentation.screens.library.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
import ua.acclorite.book_story.presentation.components.MoreDropDown
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryState
/**
* Library Top Bar.
* Contains [AnimatedTopAppBar] and [LibraryTabRow].
*
* @param state [LibraryState].
* @param pagerState [PagerState].
* @param onEvent [LibraryEvent] callback.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LibraryTopBar(
state: State<LibraryState>,
pagerState: PagerState,
onEvent: (LibraryEvent) -> Unit
) {
val focusRequester = remember { FocusRequester() }
Column(Modifier.fillMaxWidth()) {
AnimatedTopAppBar(
scrollBehavior = null,
isTopBarScrolled = state.value.hasSelectedItems,
content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
content1NavigationIcon = {},
content1Title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(id = R.string.library_screen))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = state.value.books.size.toString(),
modifier = Modifier
.background(
MaterialTheme.colorScheme.surfaceContainer,
RoundedCornerShape(14.dp)
)
.padding(horizontal = 8.dp, vertical = 3.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 16.sp
)
}
},
content1Actions = {
CustomIconButton(
icon = Icons.Default.Search,
contentDescription = R.string.search_content_desc,
disableOnClick = true,
) {
onEvent(LibraryEvent.OnSearchShowHide)
}
MoreDropDown()
},
content2Visibility = state.value.hasSelectedItems,
content2NavigationIcon = {
CustomIconButton(
icon = Icons.Default.Clear,
contentDescription = R.string.clear_selected_items_content_desc,
disableOnClick = true
) {
onEvent(LibraryEvent.OnClearSelectedBooks)
}
},
content2Title = {
Text(
stringResource(
id = R.string.selected_items_count_query,
state.value.selectedItemsCount.coerceAtLeast(1)
),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
},
content2Actions = {
CustomIconButton(
icon = Icons.AutoMirrored.Outlined.DriveFileMove,
contentDescription = R.string.move_books_content_desc,
enabled = !state.value.isLoading
&& !state.value.isRefreshing
&& !state.value.showMoveDialog,
disableOnClick = false,
) {
onEvent(LibraryEvent.OnShowHideMoveDialog)
}
CustomIconButton(
icon = Icons.Outlined.Delete,
contentDescription = R.string.delete_books_content_desc,
enabled = !state.value.isLoading
&& !state.value.isRefreshing
&& !state.value.showDeleteDialog,
disableOnClick = false
) {
onEvent(LibraryEvent.OnShowHideDeleteDialog)
}
},
content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
content3NavigationIcon = {
CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = R.string.exit_search_content_desc,
disableOnClick = true
) {
onEvent(
LibraryEvent.OnSearchShowHide
)
}
},
content3Title = {
CustomSearchTextField(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
onEvent(LibraryEvent.OnRequestFocus(focusRequester))
},
query = state.value.searchQuery,
onQueryChange = {
onEvent(LibraryEvent.OnSearchQueryChange(it))
},
onSearch = {
onEvent(LibraryEvent.OnSearch)
},
placeholder = stringResource(
id = R.string.search_query,
stringResource(id = R.string.books)
)
)
},
content3Actions = {
MoreDropDown()
}
)
LibraryTabRow(
onEvent = onEvent,
books = state.value.categorizedBooks,
pagerState = pagerState
)
}
}