Use Compose on BrowseSourceScreens (#7901)

This commit is contained in:
Andreas
2022-08-31 20:41:35 +02:00
committed by GitHub
parent bb54a81ef0
commit d4b764fa31
51 changed files with 1760 additions and 2024 deletions

View File

@@ -0,0 +1,67 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.BrowseLatestToolbar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
@Composable
fun BrowseLatestScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
val onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
Scaffold(
topBar = {
BrowseLatestToolbar(
navigateUp = navigateUp,
source = presenter.source!!,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
onHelpClick = onHelpClick,
onWebViewClick = onWebViewClick,
)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
getMangaState = { presenter.getManga(it) },
columns = columns,
displayMode = presenter.displayMode,
snackbarHostState = remember { SnackbarHostState() },
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}

View File

@@ -0,0 +1,210 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.widget.EmptyView
@Composable
fun BrowseSourceScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
val onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
Scaffold(
topBar = {
BrowseSourceToolbar(
state = presenter,
source = presenter.source!!,
displayMode = presenter.displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { presenter.search() },
)
},
floatingActionButton = {
if (presenter.filters.isNotEmpty()) {
ExtendedFloatingActionButton(
modifier = Modifier.navigationBarsPadding(),
text = { Text(text = stringResource(id = R.string.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
}
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { paddingValues ->
BrowseSourceContent(
source = presenter.source,
mangaList = mangaList,
getMangaState = { presenter.getManga(it) },
columns = columns,
displayMode = presenter.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}
@Composable
fun BrowseSourceContent(
source: CatalogueSource?,
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells,
displayMode: LibraryDisplayMode,
snackbarHostState: SnackbarHostState,
contentPadding: PaddingValues,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
onLocalSourceHelpClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
val context = LocalContext.current
val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error }
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
val getErrorMessage: (LoadState.Error) -> String = { state ->
when {
state.error is NoResultsException -> context.getString(R.string.no_results_found)
state.error.message == null -> ""
state.error.message!!.startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}"
else -> state.error.message!!
}
}
LaunchedEffect(errorState) {
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh),
duration = SnackbarDuration.Indefinite,
)
when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> mangaList.refresh()
}
}
}
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen(
message = getErrorMessage(errorState),
actions = if (source is LocalSource) {
listOf(
EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
)
} else {
listOf(
EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp) { mangaList.refresh() },
EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { onWebViewClick() },
EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { onHelpClick() },
)
},
)
return
}
when (displayMode) {
LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid(
mangaList = mangaList,
getMangaState = getMangaState,
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
LibraryDisplayMode.List -> {
BrowseSourceList(
mangaList = mangaList,
getMangaState = getMangaState,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
else -> {
BrowseSourceCompactGrid(
mangaList = mangaList,
getMangaState = getMangaState,
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
}
}

View File

@@ -0,0 +1,37 @@
package eu.kanade.presentation.browse
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.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
@Stable
interface BrowseSourceState {
val source: CatalogueSource?
var searchQuery: String?
val currentQuery: String
val filters: FilterList
val filterItems: List<IFlexible<*>>
val appliedFilters: FilterList
var dialog: BrowseSourcePresenter.Dialog?
}
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
return BrowseSourceStateImpl(initialQuery)
}
class BrowseSourceStateImpl(initialQuery: String?) : BrowseSourceState {
override var source: CatalogueSource? by mutableStateOf(null)
override var searchQuery: String? by mutableStateOf(initialQuery)
override var currentQuery: String by mutableStateOf(initialQuery ?: "")
override var filters: FilterList by mutableStateOf(FilterList())
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
override var appliedFilters by mutableStateOf(FilterList())
override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
}

View File

@@ -0,0 +1,22 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Composable
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
@Composable
fun SourceSearchScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onFabClick: () -> Unit,
onClickManga: (Manga) -> Unit,
) {
BrowseSourceScreen(
presenter = presenter,
navigateUp = navigateUp,
onDisplayModeChange = { presenter.displayMode = (it) },
onFabClick = onFabClick,
onMangaClick = onClickManga,
onMangaLongClick = onClickManga,
)
}

View File

@@ -0,0 +1,105 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.ViewModule
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
@Composable
fun BrowseLatestToolbar(
navigateUp: () -> Unit,
source: CatalogueSource,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
onHelpClick: () -> Unit,
onWebViewClick: () -> Unit,
) {
AppBar(
navigateUp = navigateUp,
title = source.name,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOf(
AppBar.Action(
title = "display_mode",
icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (source is LocalSource) {
AppBar.Action(
title = "help",
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = "webview",
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
},
),
)
DropdownMenu(
expanded = selectingDisplayMode,
onDismissRequest = { selectingDisplayMode = false },
) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.ComfortableGrid) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_grid)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.CompactGrid) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_list)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.List) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.List) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
}
},
)
}

View File

@@ -0,0 +1,106 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaGridComfortableText
import eu.kanade.presentation.library.components.MangaGridCover
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@Composable
fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells,
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
LazyVerticalGrid(
columns = columns,
contentPadding = PaddingValues(8.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.prepend is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga)
BrowseSourceComfortableGridItem(
manga = manga,
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
}
}
@Composable
fun BrowseSourceComfortableGridItem(
manga: Manga,
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
Column(
modifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
),
) {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = manga.thumbnailUrl,
)
},
badgesStart = {
if (manga.favorite) {
Badge(text = stringResource(id = R.string.in_library))
}
},
)
MangaGridComfortableText(
text = manga.title,
)
}
}

View File

@@ -0,0 +1,129 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaGridCompactText
import eu.kanade.presentation.library.components.MangaGridCover
import eu.kanade.presentation.util.plus
import eu.kanade.tachiyomi.R
@Composable
fun BrowseSourceCompactGrid(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
columns: GridCells,
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
LazyVerticalGrid(
columns = columns,
contentPadding = PaddingValues(8.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.prepend is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga)
BrowseSourceCompactGridItem(
manga = manga,
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
}
}
@Composable
fun BrowseSourceCompactGridItem(
manga: Manga,
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
MangaGridCover(
modifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
),
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxHeight()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = eu.kanade.domain.manga.model.MangaCover(
manga.id,
manga.source,
manga.favorite,
manga.thumbnailUrl,
manga.coverLastModified,
),
)
},
badgesStart = {
if (manga.favorite) {
Badge(text = stringResource(id = R.string.in_library))
}
},
content = {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
MangaGridCompactText(manga.title)
},
)
}

View File

@@ -0,0 +1,41 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
@Composable
fun RemoveMangaDialog(
onDismissRequest: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
onDismissRequest()
onConfirm()
},
) {
Text(text = stringResource(id = R.string.action_remove))
}
},
title = {
Text(text = stringResource(id = R.string.are_you_sure))
},
text = {
Text(text = stringResource(R.string.remove_manga))
},
)
}

View File

@@ -0,0 +1,94 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.items
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.Badge
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.library.components.MangaListItem
import eu.kanade.presentation.library.components.MangaListItemContent
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.verticalPadding
import eu.kanade.tachiyomi.R
@Composable
fun BrowseSourceList(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
) {
LazyColumn(
contentPadding = contentPadding,
) {
item {
if (mangaList.loadState.prepend is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
items(mangaList) { initialManga ->
initialManga ?: return@items
val manga by getMangaState(initialManga)
BrowseSourceListItem(
manga = manga,
onClick = { onMangaClick(manga) },
onLongClick = { onMangaLongClick(manga) },
)
}
item {
if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
BrowseSourceLoadingItem()
}
}
}
}
@Composable
fun BrowseSourceListItem(
manga: Manga,
onClick: () -> Unit = {},
onLongClick: () -> Unit = onClick,
) {
val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f)
MangaListItem(
coverContent = {
MangaCover.Square(
modifier = Modifier
.padding(vertical = verticalPadding)
.fillMaxHeight()
.drawWithContent {
drawContent()
if (manga.favorite) {
drawRect(overlayColor)
}
},
data = manga.thumbnailUrl,
)
},
onClick = onClick,
onLongClick = onLongClick,
badges = {
if (manga.favorite) {
Badge(text = stringResource(id = R.string.in_library))
}
},
content = {
MangaListItemContent(text = manga.title)
},
)
}

View File

@@ -0,0 +1,25 @@
package eu.kanade.presentation.browse.components
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.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun BrowseSourceLoadingItem() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(64.dp),
)
}
}

View File

@@ -0,0 +1,206 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
import kotlinx.coroutines.delay
@Composable
fun BrowseSourceToolbar(
state: BrowseSourceState,
source: CatalogueSource,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
onSearch: () -> Unit,
) {
if (state.searchQuery == null) {
BrowseSourceRegularToolbar(
source = source,
displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp,
onSearchClick = { state.searchQuery = "" },
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
)
} else {
BrowseSourceSearchToolbar(
searchQuery = state.searchQuery!!,
onSearchQueryChanged = { state.searchQuery = it },
navigateUp = {
state.searchQuery = null
onSearch()
},
onResetClick = { state.searchQuery = "" },
onSearchClick = onSearch,
)
}
}
@Composable
fun BrowseSourceRegularToolbar(
source: CatalogueSource,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit,
onSearchClick: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
) {
AppBar(
navigateUp = navigateUp,
title = source.name,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOf(
AppBar.Action(
title = "search",
icon = Icons.Outlined.Search,
onClick = onSearchClick,
),
AppBar.Action(
title = "display_mode",
icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (source is LocalSource) {
AppBar.Action(
title = "help",
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = "webview",
icon = Icons.Outlined.Public,
onClick = onWebViewClick,
)
},
),
)
DropdownMenu(
expanded = selectingDisplayMode,
onDismissRequest = { selectingDisplayMode = false },
) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.ComfortableGrid) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_grid)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.CompactGrid) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.action_display_list)) },
onClick = { onDisplayModeChange(LibraryDisplayMode.List) },
trailingIcon = {
if (displayMode == LibraryDisplayMode.List) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = "",
)
}
},
)
}
},
)
}
@Composable
fun BrowseSourceSearchToolbar(
searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
navigateUp: () -> Unit,
onResetClick: () -> Unit,
onSearchClick: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
AppBar(
navigateUp = navigateUp,
titleContent = {
BasicTextField(
value = searchQuery,
onValueChange = onSearchQueryChanged,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearchClick()
},
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
)
},
actions = {
AppBarActions(
actions = listOf(
AppBar.Action(
title = "clear",
icon = Icons.Outlined.Clear,
onClick = onResetClick,
),
),
)
},
)
LaunchedEffect(Unit) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(100)
focusRequester.requestFocus()
}
}