Use Compose for Library screen (#7557)

- Move Pager to Compose
- Move AppBar to Compose
- Use Stable interface for state
- Use pills for no. of manga in category instead of (x)
This commit is contained in:
Andreas
2022-07-23 01:05:50 +02:00
committed by GitHub
parent e8b7743826
commit 2b8d1bcc02
21 changed files with 973 additions and 687 deletions

View File

@@ -38,8 +38,8 @@ fun Badge(
) {
Box(
modifier = Modifier
.background(color)
.clip(shape),
.clip(shape)
.background(color),
) {
Text(
text = text,

View File

@@ -195,3 +195,91 @@ private fun RowScope.Button(
}
}
}
@Composable
fun LibraryBottomActionMenu(
visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: (() -> Unit)?,
onMarkAsReadClicked: (() -> Unit)?,
onMarkAsUnreadClicked: (() -> Unit)?,
onDownloadClicked: (() -> Unit)?,
onDeleteClicked: (() -> Unit)?,
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(expandFrom = Alignment.Bottom),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
) {
val scope = rememberCoroutineScope()
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.large,
tonalElevation = 3.dp,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1000)
if (isActive) confirm[toConfirmIndex] = false
}
}
Row(
modifier = Modifier
.navigationBarsPadding()
.padding(horizontal = 8.dp, vertical = 12.dp),
) {
if (onChangeCategoryClicked != null) {
Button(
title = stringResource(R.string.action_move_category),
icon = Icons.Default.BookmarkAdd,
toConfirm = confirm[0],
onLongClick = { onLongClickItem(0) },
onClick = onChangeCategoryClicked,
)
}
if (onMarkAsReadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_read),
icon = Icons.Default.DoneAll,
toConfirm = confirm[1],
onLongClick = { onLongClickItem(1) },
onClick = onMarkAsReadClicked,
)
}
if (onMarkAsUnreadClicked != null) {
Button(
title = stringResource(R.string.action_mark_as_unread),
icon = Icons.Default.RemoveDone,
toConfirm = confirm[2],
onLongClick = { onLongClickItem(2) },
onClick = onMarkAsUnreadClicked,
)
}
if (onDownloadClicked != null) {
Button(
title = stringResource(R.string.action_download),
icon = Icons.Outlined.Download,
toConfirm = confirm[3],
onLongClick = { onLongClickItem(3) },
onClick = onDownloadClicked,
)
}
if (onDeleteClicked != null) {
Button(
title = stringResource(R.string.action_delete),
icon = Icons.Outlined.Delete,
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
onClick = onDeleteClicked,
)
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@Composable
fun Pill(
text: String,
modifier: Modifier = Modifier,
color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.background,
contentColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onBackground,
elevation: Dp = 1.dp,
fontSize: TextUnit = LocalTextStyle.current.fontSize,
) {
androidx.compose.material3.Surface(
modifier = modifier
.padding(start = 4.dp)
.clip(RoundedCornerShape(100)),
color = color,
contentColor = contentColor,
tonalElevation = elevation,
) {
Text(
text = text,
modifier = Modifier.padding(6.dp, 1.dp),
fontSize = fontSize,
)
}
}

View File

@@ -0,0 +1,71 @@
package eu.kanade.presentation.library
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import eu.kanade.presentation.components.LibraryBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.library.components.LibraryContent
import eu.kanade.presentation.library.components.LibraryToolbar
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
@Composable
fun LibraryScreen(
presenter: LibraryPresenter,
onMangaClicked: (Long) -> Unit,
onGlobalSearchClicked: () -> Unit,
onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: () -> Unit,
onDeleteClicked: () -> Unit,
onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: () -> Unit,
) {
Scaffold(
topBar = {
val title by presenter.getToolbarTitle()
LibraryToolbar(
state = presenter,
title = title,
onClickUnselectAll = onClickUnselectAll,
onClickSelectAll = onClickSelectAll,
onClickInvertSelection = onClickInvertSelection,
onClickFilter = onClickFilter,
onClickRefresh = onClickRefresh,
)
},
bottomBar = {
LibraryBottomActionMenu(
visible = presenter.selectionMode,
onChangeCategoryClicked = onChangeCategoryClicked,
onMarkAsReadClicked = onMarkAsReadClicked,
onMarkAsUnreadClicked = onMarkAsUnreadClicked,
onDownloadClicked = onDownloadClicked,
onDeleteClicked = onDeleteClicked,
)
},
) { paddingValues ->
LibraryContent(
state = presenter,
contentPadding = paddingValues,
currentPage = presenter.activeCategory,
isLibraryEmpty = presenter.loadedManga.isEmpty(),
showPageTabs = presenter.tabVisibility,
showMangaCount = presenter.mangaCountVisibility,
onChangeCurrentPage = { presenter.activeCategory = it },
onMangaClicked = onMangaClicked,
onToggleSelection = { presenter.toggleSelection(it) },
onRefresh = onClickRefresh,
onGlobalSearchClicked = onGlobalSearchClicked,
getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) },
getDisplayModeForPage = { presenter.getDisplayMode(index = it) },
getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) },
getLibraryForPage = { presenter.getMangaForCategory(page = it) },
isIncognitoMode = presenter.isIncognitoMode,
isDownloadOnly = presenter.isDownloadOnly,
)
}
}

View File

@@ -0,0 +1,32 @@
package eu.kanade.presentation.library
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.category.model.Category
import eu.kanade.tachiyomi.data.database.models.LibraryManga
@Stable
interface LibraryState {
val isLoading: Boolean
val categories: List<Category>
var searchQuery: String?
val selection: List<LibraryManga>
val selectionMode: Boolean
var hasActiveFilters: Boolean
}
fun LibraryState(): LibraryState {
return LibraryStateImpl()
}
class LibraryStateImpl : LibraryState {
override var isLoading: Boolean by mutableStateOf(true)
override var categories: List<Category> by mutableStateOf(emptyList())
override var searchQuery: String? by mutableStateOf(null)
override var selection: List<LibraryManga> by mutableStateOf(emptyList())
override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() }
override var hasActiveFilters: Boolean by mutableStateOf(false)
}

View File

@@ -3,14 +3,19 @@ package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.TextButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@@ -21,10 +26,22 @@ fun LibraryComfortableGrid(
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (searchQuery.isNullOrEmpty().not()) {
TextButton(onClick = onGlobalSearchClicked) {
Text(
text = stringResource(R.string.action_global_search_query, searchQuery!!),
modifier = Modifier.zIndex(99f),
)
}
}
}
items(
items = items,
key = {

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalTextStyle
@@ -17,8 +18,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.TextButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@@ -29,10 +34,23 @@ fun LibraryCompactGrid(
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (searchQuery.isNullOrEmpty().not()) {
TextButton(onClick = onGlobalSearchClicked) {
Text(
text = stringResource(R.string.action_global_search_query, searchQuery!!),
modifier = Modifier.zIndex(99f),
)
}
}
}
items(
items = items,
key = {

View File

@@ -0,0 +1,126 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import com.google.accompanist.pager.rememberPagerState
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.library.LibraryState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.widget.EmptyView
@Composable
fun LibraryContent(
state: LibraryState,
contentPadding: PaddingValues,
currentPage: Int,
isLibraryEmpty: Boolean,
isDownloadOnly: Boolean,
isIncognitoMode: Boolean,
showPageTabs: Boolean,
showMangaCount: Boolean,
onChangeCurrentPage: (Int) -> Unit,
onMangaClicked: (Long) -> Unit,
onToggleSelection: (LibraryManga) -> Unit,
onRefresh: () -> Unit,
onGlobalSearchClicked: () -> Unit,
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
getDisplayModeForPage: @Composable (Int) -> State<DisplayModeSetting>,
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
getLibraryForPage: @Composable (Int) -> State<List<LibraryItem>>,
) {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val pagerState = rememberPagerState(currentPage)
val categories = state.categories
if (categories.isEmpty()) {
LoadingScreen()
return
}
Column(
modifier = Modifier.padding(contentPadding),
) {
if (showPageTabs && categories.size > 1) {
LibraryTabs(
state = pagerState,
categories = state.categories,
showMangaCount = showMangaCount,
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
isDownloadOnly = isDownloadOnly,
isIncognitoMode = isIncognitoMode,
)
}
val onClickManga = { manga: LibraryManga ->
if (state.selectionMode.not()) {
onMangaClicked(manga.id!!)
} else {
onToggleSelection(manga)
}
}
val onLongClickManga = { manga: LibraryManga ->
onToggleSelection(manga)
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = false),
modifier = Modifier.nestedScroll(nestedScrollInterop),
onRefresh = onRefresh,
indicator = { s, trigger ->
SwipeRefreshIndicator(
state = s,
refreshTriggerDistance = trigger,
)
},
) {
if (state.searchQuery.isNullOrEmpty() && isLibraryEmpty) {
val context = LocalContext.current
EmptyScreen(
R.string.information_empty_library,
listOf(
EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) {
context.openInBrowser("https://tachiyomi.org/help/guides/getting-started")
},
),
)
return@SwipeRefresh
}
LibraryPager(
state = pagerState,
pageCount = categories.size,
selectedManga = state.selection,
getDisplayModeForPage = getDisplayModeForPage,
getColumnsForOrientation = getColumnsForOrientation,
getLibraryForPage = getLibraryForPage,
onClickManga = onClickManga,
onLongClickManga = onLongClickManga,
onGlobalSearchClicked = onGlobalSearchClicked,
searchQuery = state.searchQuery,
)
}
LaunchedEffect(pagerState.currentPage) {
onChangeCurrentPage(pagerState.currentPage)
}
}
}

View File

@@ -1,9 +1,15 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.zIndex
import eu.kanade.presentation.components.TextButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
@@ -14,10 +20,22 @@ fun LibraryCoverOnlyGrid(
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
) {
LazyLibraryGrid(
columns = columns,
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (searchQuery.isNullOrEmpty().not()) {
TextButton(onClick = onGlobalSearchClicked) {
Text(
text = stringResource(R.string.action_global_search_query, searchQuery!!),
modifier = Modifier.zIndex(99f),
)
}
}
}
items(
items = items,
key = {

View File

@@ -17,9 +17,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import eu.kanade.domain.manga.model.MangaCover
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.BadgeGroup
import eu.kanade.presentation.components.TextButton
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.util.verticalPadding
@@ -33,10 +35,23 @@ fun LibraryList(
selection: List<LibraryManga>,
onClick: (LibraryManga) -> Unit,
onLongClick: (LibraryManga) -> Unit,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
) {
LazyColumn(
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
item {
if (searchQuery.isNullOrEmpty().not()) {
TextButton(onClick = onGlobalSearchClicked) {
Text(
text = stringResource(R.string.action_global_search_query, searchQuery!!),
modifier = Modifier.zIndex(99f),
)
}
}
}
items(
items = items,
key = {

View File

@@ -0,0 +1,96 @@
package eu.kanade.presentation.library.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
@Composable
fun LibraryPager(
state: PagerState,
pageCount: Int,
selectedManga: List<LibraryManga>,
searchQuery: String?,
onGlobalSearchClicked: () -> Unit,
getDisplayModeForPage: @Composable (Int) -> State<DisplayModeSetting>,
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
getLibraryForPage: @Composable (Int) -> State<List<LibraryItem>>,
onClickManga: (LibraryManga) -> Unit,
onLongClickManga: (LibraryManga) -> Unit,
) {
HorizontalPager(
count = pageCount,
modifier = Modifier.fillMaxSize(),
state = state,
verticalAlignment = Alignment.Top,
) { page ->
val library by getLibraryForPage(page)
val displayMode by getDisplayModeForPage(page)
val columns by if (displayMode != DisplayModeSetting.LIST) {
val configuration = LocalConfiguration.current
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
remember(isLandscape) { getColumnsForOrientation(isLandscape) }
} else {
remember { mutableStateOf(0) }
}
when (displayMode) {
DisplayModeSetting.LIST -> {
LibraryList(
items = library,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
DisplayModeSetting.COMPACT_GRID -> {
LibraryCompactGrid(
items = library,
columns = columns,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
DisplayModeSetting.COMFORTABLE_GRID -> {
LibraryComfortableGrid(
items = library,
columns = columns,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
DisplayModeSetting.COVER_ONLY_GRID -> {
LibraryCoverOnlyGrid(
items = library,
columns = columns,
selection = selectedManga,
onClick = onClickManga,
onLongClick = onLongClickManga,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
}
}
}
}

View File

@@ -0,0 +1,77 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.PagerState
import eu.kanade.domain.category.model.Category
import eu.kanade.presentation.components.DownloadedOnlyModeBanner
import eu.kanade.presentation.components.IncognitoModeBanner
import eu.kanade.presentation.components.Pill
import kotlinx.coroutines.launch
@Composable
fun LibraryTabs(
state: PagerState,
categories: List<Category>,
showMangaCount: Boolean,
isDownloadOnly: Boolean,
isIncognitoMode: Boolean,
getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
) {
val scope = rememberCoroutineScope()
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
Column {
ScrollableTabRow(
selectedTabIndex = state.currentPage,
edgePadding = 0.dp,
) {
categories.forEachIndexed { index, category ->
val count by if (showMangaCount) {
getNumberOfMangaForCategory(category.id)
} else {
remember { mutableStateOf<Int?>(null) }
}
Tab(
selected = state.currentPage == index,
onClick = { scope.launch { state.animateScrollToPage(index) } },
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = category.name)
if (count != null) {
Pill(
text = "$count",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 10.sp,
)
}
}
},
)
}
}
if (isDownloadOnly) {
DownloadedOnlyModeBanner()
}
if (isIncognitoMode) {
IncognitoModeBanner()
}
}
}

View File

@@ -0,0 +1,188 @@
package eu.kanade.presentation.library.components
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.Pill
import eu.kanade.presentation.library.LibraryState
import eu.kanade.presentation.theme.active
import kotlinx.coroutines.delay
@Composable
fun LibraryToolbar(
state: LibraryState,
title: LibraryToolbarTitle,
onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: () -> Unit,
) = when {
state.searchQuery != null -> LibrarySearchToolbar(
searchQuery = state.searchQuery!!,
onChangeSearchQuery = { state.searchQuery = it },
onClickCloseSearch = { state.searchQuery = null },
)
state.selectionMode -> LibrarySelectionToolbar(
state = state,
onClickUnselectAll = onClickUnselectAll,
onClickSelectAll = onClickSelectAll,
onClickInvertSelection = onClickInvertSelection,
)
else -> LibraryRegularToolbar(
title = title,
hasFilters = state.hasActiveFilters,
onClickSearch = { state.searchQuery = "" },
onClickFilter = onClickFilter,
onClickRefresh = onClickRefresh,
)
}
@Composable
fun LibraryRegularToolbar(
title: LibraryToolbarTitle,
hasFilters: Boolean,
onClickSearch: () -> Unit,
onClickFilter: () -> Unit,
onClickRefresh: () -> Unit,
) {
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
SmallTopAppBar(
modifier = Modifier.statusBarsPadding(),
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = title.text,
maxLines = 1,
modifier = Modifier.weight(1f, false),
overflow = TextOverflow.Ellipsis,
)
if (title.numberOfManga != null) {
Pill(
text = "${title.numberOfManga}",
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
fontSize = 14.sp,
)
}
}
},
actions = {
IconButton(onClick = onClickSearch) {
Icon(Icons.Outlined.Search, contentDescription = "search")
}
IconButton(onClick = onClickFilter) {
Icon(Icons.Outlined.FilterList, contentDescription = "search", tint = filterTint)
}
IconButton(onClick = onClickRefresh) {
Icon(Icons.Outlined.Refresh, contentDescription = "search")
}
},
)
}
@Composable
fun LibrarySelectionToolbar(
state: LibraryState,
onClickUnselectAll: () -> Unit,
onClickSelectAll: () -> Unit,
onClickInvertSelection: () -> Unit,
) {
val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f)
SmallTopAppBar(
modifier = Modifier
.drawBehind {
drawRect(backgroundColor.copy(alpha = 1f))
}
.statusBarsPadding(),
navigationIcon = {
IconButton(onClick = onClickUnselectAll) {
Icon(Icons.Outlined.Close, contentDescription = "close")
}
},
title = {
Text(text = "${state.selection.size}")
},
actions = {
IconButton(onClick = onClickSelectAll) {
Icon(Icons.Outlined.SelectAll, contentDescription = "search")
}
IconButton(onClick = onClickInvertSelection) {
Icon(Icons.Outlined.FlipToBack, contentDescription = "invert")
}
},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
),
)
}
@Composable
fun LibrarySearchToolbar(
searchQuery: String,
onChangeSearchQuery: (String) -> Unit,
onClickCloseSearch: () -> Unit,
) {
val focusRequester = remember { FocusRequester.Default }
SmallTopAppBar(
modifier = Modifier.statusBarsPadding(),
navigationIcon = {
IconButton(onClick = onClickCloseSearch) {
Icon(Icons.Outlined.ArrowBack, contentDescription = "back")
}
},
title = {
BasicTextField(
value = searchQuery,
onValueChange = onChangeSearchQuery,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(100)
focusRequester.requestFocus()
}
},
)
}
data class LibraryToolbarTitle(
val text: String,
val numberOfManga: Int? = null,
)

View File

@@ -0,0 +1,12 @@
package eu.kanade.presentation.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
val ColorScheme.active: Color
@Composable
get() {
return if (isSystemInDarkTheme()) Color(255, 235, 59) else Color(255, 193, 7)
}