package eu.kanade.presentation.browse import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable 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.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.domain.manga.model.Manga import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.util.bottomNavPaddingValues import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedPresenter import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch sealed class SourceFeedUI { abstract val id: Long abstract val title: String @Composable @ReadOnlyComposable get abstract val results: List? abstract fun withResults(results: List?): SourceFeedUI data class Latest(override val results: List?) : SourceFeedUI() { override val id: Long = -1 override val title: String @Composable @ReadOnlyComposable get() = stringResource(R.string.latest) override fun withResults(results: List?): SourceFeedUI { return copy(results = results) } } data class Browse(override val results: List?) : SourceFeedUI() { override val id: Long = -2 override val title: String @Composable @ReadOnlyComposable get() = stringResource(R.string.browse) override fun withResults(results: List?): SourceFeedUI { return copy(results = results) } } data class SourceSavedSearch( val feed: FeedSavedSearch, val savedSearch: SavedSearch, override val results: List?, ) : SourceFeedUI() { override val id: Long get() = feed.id override val title: String @Composable @ReadOnlyComposable get() = savedSearch.name override fun withResults(results: List?): SourceFeedUI { return copy(results = results) } } } @Composable fun SourceFeedScreen( presenter: SourceFeedPresenter, onClickBrowse: () -> Unit, onClickLatest: () -> Unit, onClickSavedSearch: (SavedSearch) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, ) { Scaffold( topBar = { scrollBehavior -> SourceFeedToolbar( title = presenter.source.name, state = presenter, scrollBehavior = scrollBehavior, incognitoMode = presenter.isIncognitoMode, downloadedOnlyMode = presenter.isDownloadOnly, ) }, ) { paddingValues -> Crossfade(targetState = presenter.isLoading) { state -> when (state) { true -> LoadingScreen() false -> { SourceFeedList( state = presenter, paddingValues = paddingValues, onClickBrowse = onClickBrowse, onClickLatest = onClickLatest, onClickSavedSearch = onClickSavedSearch, onClickDelete = onClickDelete, onClickManga = onClickManga, ) } } } } } @Composable fun SourceFeedList( state: SourceFeedState, paddingValues: PaddingValues, onClickBrowse: () -> Unit, onClickLatest: () -> Unit, onClickSavedSearch: (SavedSearch) -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, ) { ScrollbarLazyColumn( contentPadding = paddingValues + bottomNavPaddingValues + WindowInsets.navigationBars.only(WindowInsetsSides.Vertical).asPaddingValues() + topPaddingValues, ) { items( state.items.orEmpty(), key = { it.id }, ) { item -> SourceFeedItem( modifier = Modifier.animateItemPlacement(), item = item, onClickTitle = when (item) { is SourceFeedUI.Browse -> onClickBrowse is SourceFeedUI.Latest -> onClickLatest is SourceFeedUI.SourceSavedSearch -> { { onClickSavedSearch(item.savedSearch) } } }, onClickDelete = onClickDelete, onClickManga = onClickManga, ) } } } @Composable fun SourceFeedItem( modifier: Modifier, item: SourceFeedUI, onClickTitle: () -> Unit, onClickDelete: (FeedSavedSearch) -> Unit, onClickManga: (Manga) -> Unit, ) { Column( modifier then Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { Row( Modifier .fillMaxWidth() .let { if (item is SourceFeedUI.SourceSavedSearch) { it.combinedClickable( onLongClick = { onClickDelete(item.feed) }, onClick = onClickTitle, ) } else { it.clickable(onClick = onClickTitle) } }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.padding(start = 16.dp)) { Text( text = item.title, style = MaterialTheme.typography.bodyMedium, ) } Icon( painter = painterResource(R.drawable.ic_arrow_forward_24dp), contentDescription = stringResource(R.string.label_more), modifier = Modifier.padding(16.dp), ) } val results = item.results when { results == null -> { CircularProgressIndicator() } results.isEmpty() -> { Text(stringResource(R.string.no_results_found), modifier = Modifier.padding(bottom = 16.dp)) } else -> { LazyRow( Modifier.fillMaxWidth(), contentPadding = PaddingValues(horizontal = 12.dp), ) { items(results) { FeedCardItem( manga = it, onClickManga = onClickManga, ) } } } } } } @Composable fun SourceFeedToolbar( title: String, state: SourceFeedState, scrollBehavior: TopAppBarScrollBehavior, incognitoMode: Boolean, downloadedOnlyMode: Boolean, ) { when { state.searchQuery != null -> SearchToolbar( searchQuery = state.searchQuery!!, onChangeSearchQuery = { state.searchQuery = it }, onClickCloseSearch = { state.searchQuery = null }, onClickResetSearch = { state.searchQuery = "" }, scrollBehavior = scrollBehavior, incognitoMode = incognitoMode, downloadedOnlyMode = downloadedOnlyMode, ) else -> AppBar( title = title, incognitoMode = incognitoMode, downloadedOnlyMode = downloadedOnlyMode, scrollBehavior = scrollBehavior, actions = { IconButton(onClick = { state.searchQuery = "" }) { Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search)) } }, ) } }