mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-15 13:37:29 +01:00
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:
@@ -38,8 +38,8 @@ fun Badge(
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color)
|
||||
.clip(shape),
|
||||
.clip(shape)
|
||||
.background(color),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
app/src/main/java/eu/kanade/presentation/components/Pill.kt
Normal file
38
app/src/main/java/eu/kanade/presentation/components/Pill.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
12
app/src/main/java/eu/kanade/presentation/theme/Color.kt
Normal file
12
app/src/main/java/eu/kanade/presentation/theme/Color.kt
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user