mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 03:58:56 +01:00
Use Compose on BrowseSourceScreens (#7901)
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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 = "",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user