mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Use Voyager on BrowseSource and SourceSearch screen (#8650)
Some navigation janks will be dealt with when the migration is complete
This commit is contained in:
		@@ -1,213 +1,37 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.horizontalScroll
 | 
			
		||||
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.navigationBarsPadding
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.rememberScrollState
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.Favorite
 | 
			
		||||
import androidx.compose.material.icons.outlined.FilterList
 | 
			
		||||
import androidx.compose.material.icons.outlined.HelpOutline
 | 
			
		||||
import androidx.compose.material.icons.outlined.NewReleases
 | 
			
		||||
import androidx.compose.material.icons.outlined.Public
 | 
			
		||||
import androidx.compose.material.icons.outlined.Refresh
 | 
			
		||||
import androidx.compose.material3.FilterChip
 | 
			
		||||
import androidx.compose.material3.FilterChipDefaults
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
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.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
import androidx.paging.compose.LazyPagingItems
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import eu.kanade.data.source.NoResultsException
 | 
			
		||||
import eu.kanade.domain.library.model.LibraryDisplayMode
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
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.AppStateBanners
 | 
			
		||||
import eu.kanade.presentation.components.Divider
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreenAction
 | 
			
		||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
 | 
			
		||||
import eu.kanade.presentation.components.LoadingScreen
 | 
			
		||||
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.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    openFilterSheet: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
    incognitoMode: Boolean,
 | 
			
		||||
    downloadedOnlyMode: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
 | 
			
		||||
    val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
 | 
			
		||||
 | 
			
		||||
    val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = {
 | 
			
		||||
            Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
 | 
			
		||||
                BrowseSourceToolbar(
 | 
			
		||||
                    state = presenter,
 | 
			
		||||
                    source = presenter.source,
 | 
			
		||||
                    displayMode = presenter.displayMode,
 | 
			
		||||
                    onDisplayModeChange = { presenter.displayMode = it },
 | 
			
		||||
                    navigateUp = navigateUp,
 | 
			
		||||
                    onWebViewClick = onWebViewClick,
 | 
			
		||||
                    onHelpClick = onHelpClick,
 | 
			
		||||
                    onSearch = { presenter.search(it) },
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                Row(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .horizontalScroll(rememberScrollState())
 | 
			
		||||
                        .padding(horizontal = 8.dp),
 | 
			
		||||
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
                ) {
 | 
			
		||||
                    FilterChip(
 | 
			
		||||
                        selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
 | 
			
		||||
                        onClick = {
 | 
			
		||||
                            presenter.reset()
 | 
			
		||||
                            presenter.search(GetRemoteManga.QUERY_POPULAR)
 | 
			
		||||
                        },
 | 
			
		||||
                        leadingIcon = {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Favorite,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                                modifier = Modifier
 | 
			
		||||
                                    .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                            )
 | 
			
		||||
                        },
 | 
			
		||||
                        label = {
 | 
			
		||||
                            Text(text = stringResource(R.string.popular))
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    if (presenter.source?.supportsLatest == true) {
 | 
			
		||||
                        FilterChip(
 | 
			
		||||
                            selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                presenter.reset()
 | 
			
		||||
                                presenter.search(GetRemoteManga.QUERY_LATEST)
 | 
			
		||||
                            },
 | 
			
		||||
                            leadingIcon = {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = Icons.Outlined.NewReleases,
 | 
			
		||||
                                    contentDescription = "",
 | 
			
		||||
                                    modifier = Modifier
 | 
			
		||||
                                        .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            label = {
 | 
			
		||||
                                Text(text = stringResource(R.string.latest))
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    if (presenter.filters.isNotEmpty()) {
 | 
			
		||||
                        FilterChip(
 | 
			
		||||
                            selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
 | 
			
		||||
                            onClick = openFilterSheet,
 | 
			
		||||
                            leadingIcon = {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = Icons.Outlined.FilterList,
 | 
			
		||||
                                    contentDescription = "",
 | 
			
		||||
                                    modifier = Modifier
 | 
			
		||||
                                        .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            label = {
 | 
			
		||||
                                Text(text = stringResource(R.string.action_filter))
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Divider()
 | 
			
		||||
 | 
			
		||||
                AppStateBanners(downloadedOnlyMode, incognitoMode)
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        snackbarHost = {
 | 
			
		||||
            SnackbarHost(hostState = snackbarHostState)
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            state = presenter,
 | 
			
		||||
            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 BrowseSourceFloatingActionButton(
 | 
			
		||||
    modifier: Modifier = Modifier.navigationBarsPadding(),
 | 
			
		||||
    isVisible: Boolean,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    AnimatedVisibility(visible = isVisible) {
 | 
			
		||||
        ExtendedFloatingActionButton(
 | 
			
		||||
            modifier = modifier,
 | 
			
		||||
            text = { Text(text = stringResource(R.string.action_filter)) },
 | 
			
		||||
            icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
 | 
			
		||||
            onClick = onFabClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceContent(
 | 
			
		||||
    state: BrowseSourceState,
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    source: CatalogueSource?,
 | 
			
		||||
    mangaList: LazyPagingItems<StateFlow<Manga>>,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
@@ -249,7 +73,7 @@ fun BrowseSourceContent(
 | 
			
		||||
    if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
 | 
			
		||||
        EmptyScreen(
 | 
			
		||||
            message = getErrorMessage(errorState),
 | 
			
		||||
            actions = if (state.source is LocalSource) {
 | 
			
		||||
            actions = if (source is LocalSource) {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyScreenAction(
 | 
			
		||||
                        stringResId = R.string.local_source_help_guide,
 | 
			
		||||
@@ -290,7 +114,6 @@ fun BrowseSourceContent(
 | 
			
		||||
        LibraryDisplayMode.ComfortableGrid -> {
 | 
			
		||||
            BrowseSourceComfortableGrid(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                columns = columns,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
@@ -300,16 +123,14 @@ fun BrowseSourceContent(
 | 
			
		||||
        LibraryDisplayMode.List -> {
 | 
			
		||||
            BrowseSourceList(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        else -> {
 | 
			
		||||
        LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
 | 
			
		||||
            BrowseSourceCompactGrid(
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                getMangaState = getMangaState,
 | 
			
		||||
                columns = columns,
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
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.BrowseSourcePresenter.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
interface BrowseSourceState {
 | 
			
		||||
    val source: CatalogueSource?
 | 
			
		||||
    var searchQuery: String?
 | 
			
		||||
    val currentFilter: Filter
 | 
			
		||||
    val isUserQuery: Boolean
 | 
			
		||||
    val filters: FilterList
 | 
			
		||||
    val filterItems: List<IFlexible<*>>
 | 
			
		||||
    var dialog: BrowseSourcePresenter.Dialog?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
 | 
			
		||||
    return when (val filter = Filter.valueOf(initialQuery ?: "")) {
 | 
			
		||||
        Filter.Latest, Filter.Popular -> BrowseSourceStateImpl(initialCurrentFilter = filter)
 | 
			
		||||
        is Filter.UserInput -> BrowseSourceStateImpl(initialQuery = initialQuery, initialCurrentFilter = filter)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentFilter: Filter) : BrowseSourceState {
 | 
			
		||||
    override var source: CatalogueSource? by mutableStateOf(null)
 | 
			
		||||
    override var searchQuery: String? by mutableStateOf(initialQuery)
 | 
			
		||||
    override var currentFilter: Filter by mutableStateOf(initialCurrentFilter)
 | 
			
		||||
    override val isUserQuery: Boolean by derivedStateOf { currentFilter is Filter.UserInput && currentFilter.query.isNotEmpty() }
 | 
			
		||||
    override var filters: FilterList by mutableStateOf(FilterList())
 | 
			
		||||
    override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
 | 
			
		||||
    override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
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.LocalUriHandler
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.presentation.components.SearchToolbar
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourceSearchScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
 | 
			
		||||
    val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
 | 
			
		||||
 | 
			
		||||
    val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = { scrollBehavior ->
 | 
			
		||||
            SearchToolbar(
 | 
			
		||||
                searchQuery = presenter.searchQuery ?: "",
 | 
			
		||||
                onChangeSearchQuery = { presenter.searchQuery = it },
 | 
			
		||||
                onClickCloseSearch = navigateUp,
 | 
			
		||||
                onSearch = { presenter.search(it) },
 | 
			
		||||
                scrollBehavior = scrollBehavior,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        floatingActionButton = {
 | 
			
		||||
            BrowseSourceFloatingActionButton(
 | 
			
		||||
                isVisible = presenter.filters.isNotEmpty(),
 | 
			
		||||
                onFabClick = onFabClick,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        snackbarHost = {
 | 
			
		||||
            SnackbarHost(hostState = snackbarHostState)
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            state = presenter,
 | 
			
		||||
            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 = onMangaClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
@@ -17,11 +17,11 @@ import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.MangaComfortableGridItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceComfortableGrid(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    mangaList: LazyPagingItems<StateFlow<Manga>>,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
@@ -40,8 +40,7 @@ fun BrowseSourceComfortableGrid(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList.itemCount) { index ->
 | 
			
		||||
            val initialManga = mangaList[index] ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
            val manga by mangaList[index]?.collectAsState() ?: return@items
 | 
			
		||||
            BrowseSourceComfortableGridItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
@@ -17,11 +17,11 @@ import eu.kanade.presentation.browse.InLibraryBadge
 | 
			
		||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.MangaCompactGridItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceCompactGrid(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    mangaList: LazyPagingItems<StateFlow<Manga>>,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
@@ -40,8 +40,7 @@ fun BrowseSourceCompactGrid(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList.itemCount) { index ->
 | 
			
		||||
            val initialManga = mangaList[index] ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
            val manga by mangaList[index]?.collectAsState() ?: return@items
 | 
			
		||||
            BrowseSourceCompactGridItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.LoadState
 | 
			
		||||
@@ -15,11 +15,11 @@ import eu.kanade.presentation.components.CommonMangaItemDefaults
 | 
			
		||||
import eu.kanade.presentation.components.LazyColumn
 | 
			
		||||
import eu.kanade.presentation.components.MangaListItem
 | 
			
		||||
import eu.kanade.presentation.util.plus
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceList(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    mangaList: LazyPagingItems<StateFlow<Manga>>,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
@@ -33,9 +33,9 @@ fun BrowseSourceList(
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        items(mangaList) { initialManga ->
 | 
			
		||||
            initialManga ?: return@items
 | 
			
		||||
            val manga by getMangaState(initialManga)
 | 
			
		||||
        items(mangaList) { mangaflow ->
 | 
			
		||||
            mangaflow ?: return@items
 | 
			
		||||
            val manga by mangaflow.collectAsState()
 | 
			
		||||
            BrowseSourceListItem(
 | 
			
		||||
                manga = manga,
 | 
			
		||||
                onClick = { onMangaClick(manga) },
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.domain.library.model.LibraryDisplayMode
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceState
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.AppBarActions
 | 
			
		||||
import eu.kanade.presentation.components.AppBarTitle
 | 
			
		||||
@@ -27,7 +26,8 @@ import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceToolbar(
 | 
			
		||||
    state: BrowseSourceState,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    onSearchQueryChange: (String?) -> Unit,
 | 
			
		||||
    source: CatalogueSource?,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
@@ -44,8 +44,8 @@ fun BrowseSourceToolbar(
 | 
			
		||||
    SearchToolbar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        titleContent = { AppBarTitle(title) },
 | 
			
		||||
        searchQuery = state.searchQuery,
 | 
			
		||||
        onChangeSearchQuery = { state.searchQuery = it },
 | 
			
		||||
        searchQuery = searchQuery,
 | 
			
		||||
        onChangeSearchQuery = onSearchQueryChange,
 | 
			
		||||
        onSearch = onSearch,
 | 
			
		||||
        onClickCloseSearch = navigateUp,
 | 
			
		||||
        actions = {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,26 +2,14 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import cafe.adriel.voyager.navigator.Navigator
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.SourceSearchScreen
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.setRoot
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
 | 
			
		||||
 | 
			
		||||
class SourceSearchController(
 | 
			
		||||
    bundle: Bundle,
 | 
			
		||||
) : BrowseSourceController(bundle) {
 | 
			
		||||
class SourceSearchController(bundle: Bundle) : BasicFullComposeController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
 | 
			
		||||
        bundleOf(
 | 
			
		||||
@@ -31,49 +19,16 @@ class SourceSearchController(
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private var oldManga: Manga? = args.getSerializableCompat(MANGA_KEY)
 | 
			
		||||
    private var oldManga: Manga = args.getSerializableCompat(MANGA_KEY)!!
 | 
			
		||||
    private val sourceId = args.getLong(SOURCE_ID_KEY)
 | 
			
		||||
    private val query = args.getString(SEARCH_QUERY_KEY)
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        SourceSearchScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onFabClick = { filterSheet?.show() },
 | 
			
		||||
            onMangaClick = {
 | 
			
		||||
                presenter.dialog = BrowseSourcePresenter.Dialog.Migrate(it)
 | 
			
		||||
            },
 | 
			
		||||
            onWebViewClick = f@{
 | 
			
		||||
                val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
                activity?.let { context ->
 | 
			
		||||
                    val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
                    context.startActivity(intent)
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        when (val dialog = presenter.dialog) {
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.Migrate -> {
 | 
			
		||||
                MigrateDialog(
 | 
			
		||||
                    oldManga = oldManga!!,
 | 
			
		||||
                    newManga = dialog.newManga,
 | 
			
		||||
                    // TODO: Move screen model down into Dialog when this screen is using Voyager
 | 
			
		||||
                    screenModel = remember { MigrateDialogScreenModel() },
 | 
			
		||||
                    onDismissRequest = { presenter.dialog = null },
 | 
			
		||||
                    onClickTitle = { router.pushController(MangaController(dialog.newManga.id)) },
 | 
			
		||||
                    onPopScreen = {
 | 
			
		||||
                        // TODO: Push to manga screen and remove this and the previous screen when it moves to Voyager
 | 
			
		||||
                        router.setRoot(BrowseController(toExtensions = false), R.id.nav_browse)
 | 
			
		||||
                        router.pushController(MangaController(dialog.newManga.id))
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            else -> {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(presenter.filters) {
 | 
			
		||||
            initFilterSheet()
 | 
			
		||||
        }
 | 
			
		||||
        Navigator(screen = SourceSearchScreen(oldManga, sourceId, query))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private const val MANGA_KEY = "oldManga"
 | 
			
		||||
private const val SOURCE_ID_KEY = "sourceId"
 | 
			
		||||
private const val SEARCH_QUERY_KEY = "searchQuery"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.migration.search
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.FilterList
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import cafe.adriel.voyager.core.model.rememberScreenModel
 | 
			
		||||
import cafe.adriel.voyager.core.screen.Screen
 | 
			
		||||
import cafe.adriel.voyager.navigator.LocalNavigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceContent
 | 
			
		||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.presentation.components.SearchToolbar
 | 
			
		||||
import eu.kanade.presentation.util.LocalRouter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.setRoot
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.BrowseController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
 | 
			
		||||
data class SourceSearchScreen(
 | 
			
		||||
    private val oldManga: Manga,
 | 
			
		||||
    private val sourceId: Long,
 | 
			
		||||
    private val query: String? = null,
 | 
			
		||||
) : Screen {
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun Content() {
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
        val uriHandler = LocalUriHandler.current
 | 
			
		||||
        val router = LocalRouter.currentOrThrow
 | 
			
		||||
        val navigator = LocalNavigator.currentOrThrow
 | 
			
		||||
 | 
			
		||||
        val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
 | 
			
		||||
        val state by screenModel.state.collectAsState()
 | 
			
		||||
 | 
			
		||||
        val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
        val navigateUp: () -> Unit = {
 | 
			
		||||
            when {
 | 
			
		||||
                navigator.canPop -> navigator.pop()
 | 
			
		||||
                router.backstackSize > 1 -> router.popCurrentController()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Scaffold(
 | 
			
		||||
            topBar = { scrollBehavior ->
 | 
			
		||||
                SearchToolbar(
 | 
			
		||||
                    searchQuery = state.toolbarQuery ?: "",
 | 
			
		||||
                    onChangeSearchQuery = screenModel::setToolbarQuery,
 | 
			
		||||
                    onClickCloseSearch = navigateUp,
 | 
			
		||||
                    onSearch = { screenModel.search(it) },
 | 
			
		||||
                    scrollBehavior = scrollBehavior,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            floatingActionButton = {
 | 
			
		||||
                AnimatedVisibility(visible = state.filters.isNotEmpty()) {
 | 
			
		||||
                    ExtendedFloatingActionButton(
 | 
			
		||||
                        text = { Text(text = stringResource(R.string.action_filter)) },
 | 
			
		||||
                        icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
 | 
			
		||||
                        onClick = screenModel::openFilterSheet,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
 | 
			
		||||
        ) { paddingValues ->
 | 
			
		||||
            val mangaList = remember(state.currentFilter) {
 | 
			
		||||
                screenModel.getMangaListFlow(state.currentFilter)
 | 
			
		||||
            }.collectAsLazyPagingItems()
 | 
			
		||||
            val openMigrateDialog: (Manga) -> Unit = {
 | 
			
		||||
                screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(it))
 | 
			
		||||
            }
 | 
			
		||||
            BrowseSourceContent(
 | 
			
		||||
                source = screenModel.source,
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
 | 
			
		||||
                displayMode = screenModel.displayMode,
 | 
			
		||||
                snackbarHostState = snackbarHostState,
 | 
			
		||||
                contentPadding = paddingValues,
 | 
			
		||||
                onWebViewClick = {
 | 
			
		||||
                    val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
 | 
			
		||||
                    val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
                    context.startActivity(intent)
 | 
			
		||||
                },
 | 
			
		||||
                onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
 | 
			
		||||
                onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) },
 | 
			
		||||
                onMangaClick = openMigrateDialog,
 | 
			
		||||
                onMangaLongClick = openMigrateDialog,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        when (val dialog = state.dialog) {
 | 
			
		||||
            is BrowseSourceScreenModel.Dialog.Migrate -> {
 | 
			
		||||
                MigrateDialog(
 | 
			
		||||
                    oldManga = oldManga,
 | 
			
		||||
                    newManga = dialog.newManga,
 | 
			
		||||
                    screenModel = rememberScreenModel { MigrateDialogScreenModel() },
 | 
			
		||||
                    onDismissRequest = { screenModel.setDialog(null) },
 | 
			
		||||
                    onClickTitle = { router.pushController(MangaController(dialog.newManga.id)) },
 | 
			
		||||
                    onPopScreen = {
 | 
			
		||||
                        // TODO: Push to manga screen and remove this and the previous screen when it moves to Voyager
 | 
			
		||||
                        router.setRoot(BrowseController(toExtensions = false), R.id.nav_browse)
 | 
			
		||||
                        router.pushController(MangaController(dialog.newManga.id))
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            else -> {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(state.filters) {
 | 
			
		||||
            screenModel.initFilterSheet(context)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -49,7 +49,7 @@ fun Screen.sourcesTab(): TabContent {
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onClickItem = { source, query ->
 | 
			
		||||
                    screenModel.onOpenSource(source)
 | 
			
		||||
                    router.pushController(BrowseSourceController(source, query))
 | 
			
		||||
                    router.pushController(BrowseSourceController(source.id, query))
 | 
			
		||||
                },
 | 
			
		||||
                onClickPin = screenModel::togglePin,
 | 
			
		||||
                onLongClickItem = screenModel::showSourceDialog,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,18 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.activity.compose.BackHandler
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalHapticFeedback
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceScreen
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Dialog
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
import cafe.adriel.voyager.navigator.CurrentScreen
 | 
			
		||||
import cafe.adriel.voyager.navigator.Navigator
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
 | 
			
		||||
import kotlinx.coroutines.channels.Channel
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.consumeAsFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
    FullComposeController<BrowseSourcePresenter>(bundle) {
 | 
			
		||||
class BrowseSourceController(bundle: Bundle) : BasicFullComposeController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(sourceId: Long, query: String? = null) : this(
 | 
			
		||||
        bundleOf(
 | 
			
		||||
@@ -35,117 +21,27 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
 | 
			
		||||
    private val sourceId = args.getLong(SOURCE_ID_KEY)
 | 
			
		||||
    private val initialQuery = args.getString(SEARCH_QUERY_KEY)
 | 
			
		||||
 | 
			
		||||
    constructor(source: Source, query: String? = null) : this(source.id, query)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sheet containing filter items.
 | 
			
		||||
     */
 | 
			
		||||
    protected var filterSheet: SourceFilterSheet? = null
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
 | 
			
		||||
    }
 | 
			
		||||
    private val queryEvent = Channel<BrowseSourceScreen.SearchType>()
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
        val haptic = LocalHapticFeedback.current
 | 
			
		||||
        Navigator(screen = BrowseSourceScreen(sourceId = sourceId, query = initialQuery)) { navigator ->
 | 
			
		||||
            CurrentScreen()
 | 
			
		||||
 | 
			
		||||
        BrowseSourceScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = ::navigateUp,
 | 
			
		||||
            openFilterSheet = { filterSheet?.show() },
 | 
			
		||||
            onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onMangaLongClick = { manga ->
 | 
			
		||||
                scope.launchIO {
 | 
			
		||||
                    val duplicateManga = presenter.getDuplicateLibraryManga(manga)
 | 
			
		||||
                    when {
 | 
			
		||||
                        manga.favorite -> presenter.dialog = Dialog.RemoveManga(manga)
 | 
			
		||||
                        duplicateManga != null -> presenter.dialog = Dialog.AddDuplicateManga(manga, duplicateManga)
 | 
			
		||||
                        else -> presenter.addFavorite(manga)
 | 
			
		||||
            LaunchedEffect(Unit) {
 | 
			
		||||
                queryEvent.consumeAsFlow()
 | 
			
		||||
                    .collectLatest {
 | 
			
		||||
                        val screen = (navigator.lastItem as? BrowseSourceScreen)
 | 
			
		||||
                        when (it) {
 | 
			
		||||
                            is BrowseSourceScreen.SearchType.Genre -> screen?.searchGenre(it.txt)
 | 
			
		||||
                            is BrowseSourceScreen.SearchType.Text -> screen?.search(it.txt)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            onWebViewClick = f@{
 | 
			
		||||
                val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
                val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
                context.startActivity(intent)
 | 
			
		||||
            },
 | 
			
		||||
            incognitoMode = presenter.isIncognitoMode,
 | 
			
		||||
            downloadedOnlyMode = presenter.isDownloadOnly,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val onDismissRequest = { presenter.dialog = null }
 | 
			
		||||
        when (val dialog = presenter.dialog) {
 | 
			
		||||
            null -> {}
 | 
			
		||||
            is Dialog.Migrate -> {}
 | 
			
		||||
            is Dialog.AddDuplicateManga -> {
 | 
			
		||||
                DuplicateMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = { presenter.addFavorite(dialog.manga) },
 | 
			
		||||
                    onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
 | 
			
		||||
                    duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is Dialog.RemoveManga -> {
 | 
			
		||||
                RemoveMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                    mangaToRemove = dialog.manga,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is Dialog.ChangeMangaCategory -> {
 | 
			
		||||
                ChangeCategoryDialog(
 | 
			
		||||
                    initialSelection = dialog.initialSelection,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onEditCategories = {
 | 
			
		||||
                        router.pushController(CategoryController())
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = { include, _ ->
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                        presenter.moveMangaToCategories(dialog.manga, include)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        BackHandler(onBack = ::navigateUp)
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(presenter.filters) {
 | 
			
		||||
            initFilterSheet()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun navigateUp() {
 | 
			
		||||
        when {
 | 
			
		||||
            !presenter.isUserQuery && presenter.searchQuery != null -> presenter.searchQuery = null
 | 
			
		||||
            else -> router.popCurrentController()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun initFilterSheet() {
 | 
			
		||||
        if (presenter.filters.isEmpty()) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        filterSheet = SourceFilterSheet(
 | 
			
		||||
            activity!!,
 | 
			
		||||
            onFilterClicked = {
 | 
			
		||||
                presenter.search(filters = presenter.filters)
 | 
			
		||||
            },
 | 
			
		||||
            onResetClicked = {
 | 
			
		||||
                presenter.reset()
 | 
			
		||||
                filterSheet?.setFilters(presenter.filterItems)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filterSheet?.setFilters(presenter.filterItems)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -154,7 +50,7 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     * @param newQuery the new query.
 | 
			
		||||
     */
 | 
			
		||||
    fun searchWithQuery(newQuery: String) {
 | 
			
		||||
        presenter.search(newQuery)
 | 
			
		||||
        viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Text(newQuery)) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -165,46 +61,9 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     * @param genreName the name of the genre
 | 
			
		||||
     */
 | 
			
		||||
    fun searchWithGenre(genreName: String) {
 | 
			
		||||
        val defaultFilters = presenter.source!!.getFilterList()
 | 
			
		||||
 | 
			
		||||
        var genreExists = false
 | 
			
		||||
 | 
			
		||||
        filter@ for (sourceFilter in defaultFilters) {
 | 
			
		||||
            if (sourceFilter is Filter.Group<*>) {
 | 
			
		||||
                for (filter in sourceFilter.state) {
 | 
			
		||||
                    if (filter is Filter<*> && filter.name.equals(genreName, true)) {
 | 
			
		||||
                        when (filter) {
 | 
			
		||||
                            is Filter.TriState -> filter.state = 1
 | 
			
		||||
                            is Filter.CheckBox -> filter.state = true
 | 
			
		||||
                            else -> {}
 | 
			
		||||
                        }
 | 
			
		||||
                        genreExists = true
 | 
			
		||||
                        break@filter
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else if (sourceFilter is Filter.Select<*>) {
 | 
			
		||||
                val index = sourceFilter.values.filterIsInstance<String>()
 | 
			
		||||
                    .indexOfFirst { it.equals(genreName, true) }
 | 
			
		||||
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    sourceFilter.state = index
 | 
			
		||||
                    genreExists = true
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (genreExists) {
 | 
			
		||||
            filterSheet?.setFilters(defaultFilters.toItems())
 | 
			
		||||
 | 
			
		||||
            presenter.search(filters = defaultFilters)
 | 
			
		||||
        } else {
 | 
			
		||||
            searchWithQuery(genreName)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected companion object {
 | 
			
		||||
        const val SOURCE_ID_KEY = "sourceId"
 | 
			
		||||
        const val SEARCH_QUERY_KEY = "searchQuery"
 | 
			
		||||
        viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Genre(genreName)) }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private const val SOURCE_ID_KEY = "sourceId"
 | 
			
		||||
private const val SEARCH_QUERY_KEY = "searchQuery"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,283 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.activity.compose.BackHandler
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.horizontalScroll
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.rememberScrollState
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.outlined.Favorite
 | 
			
		||||
import androidx.compose.material.icons.outlined.FilterList
 | 
			
		||||
import androidx.compose.material.icons.outlined.NewReleases
 | 
			
		||||
import androidx.compose.material3.FilterChip
 | 
			
		||||
import androidx.compose.material3.FilterChipDefaults
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.LaunchedEffect
 | 
			
		||||
import androidx.compose.runtime.collectAsState
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalHapticFeedback
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import cafe.adriel.voyager.core.model.rememberScreenModel
 | 
			
		||||
import cafe.adriel.voyager.core.screen.Screen
 | 
			
		||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
 | 
			
		||||
import cafe.adriel.voyager.navigator.LocalNavigator
 | 
			
		||||
import cafe.adriel.voyager.navigator.currentOrThrow
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceContent
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.AppStateBanners
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.Divider
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.presentation.util.LocalRouter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchIO
 | 
			
		||||
import kotlinx.coroutines.channels.Channel
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.receiveAsFlow
 | 
			
		||||
 | 
			
		||||
data class BrowseSourceScreen(
 | 
			
		||||
    private val sourceId: Long,
 | 
			
		||||
    private val query: String? = null,
 | 
			
		||||
) : Screen {
 | 
			
		||||
 | 
			
		||||
    override val key = uniqueScreenKey
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun Content() {
 | 
			
		||||
        val router = LocalRouter.currentOrThrow
 | 
			
		||||
        val navigator = LocalNavigator.currentOrThrow
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
        val haptic = LocalHapticFeedback.current
 | 
			
		||||
        val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
        val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
 | 
			
		||||
        val state by screenModel.state.collectAsState()
 | 
			
		||||
 | 
			
		||||
        val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
        val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
 | 
			
		||||
 | 
			
		||||
        val onWebViewClick = f@{
 | 
			
		||||
            val source = screenModel.source as? HttpSource ?: return@f
 | 
			
		||||
            val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
            context.startActivity(intent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val navigateUp: () -> Unit = {
 | 
			
		||||
            when {
 | 
			
		||||
                navigator.canPop -> navigator.pop()
 | 
			
		||||
                router.backstackSize > 1 -> router.popCurrentController()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Scaffold(
 | 
			
		||||
            topBar = {
 | 
			
		||||
                Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
 | 
			
		||||
                    BrowseSourceToolbar(
 | 
			
		||||
                        searchQuery = state.toolbarQuery,
 | 
			
		||||
                        onSearchQueryChange = screenModel::setToolbarQuery,
 | 
			
		||||
                        source = screenModel.source,
 | 
			
		||||
                        displayMode = screenModel.displayMode,
 | 
			
		||||
                        onDisplayModeChange = { screenModel.displayMode = it },
 | 
			
		||||
                        navigateUp = navigateUp,
 | 
			
		||||
                        onWebViewClick = onWebViewClick,
 | 
			
		||||
                        onHelpClick = onHelpClick,
 | 
			
		||||
                        onSearch = { screenModel.search(it) },
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    Row(
 | 
			
		||||
                        modifier = Modifier
 | 
			
		||||
                            .horizontalScroll(rememberScrollState())
 | 
			
		||||
                            .padding(horizontal = 8.dp),
 | 
			
		||||
                        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
                    ) {
 | 
			
		||||
                        FilterChip(
 | 
			
		||||
                            selected = state.currentFilter == BrowseSourceScreenModel.Filter.Popular,
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                screenModel.reset()
 | 
			
		||||
                                screenModel.search(GetRemoteManga.QUERY_POPULAR)
 | 
			
		||||
                            },
 | 
			
		||||
                            leadingIcon = {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = Icons.Outlined.Favorite,
 | 
			
		||||
                                    contentDescription = "",
 | 
			
		||||
                                    modifier = Modifier
 | 
			
		||||
                                        .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            label = {
 | 
			
		||||
                                Text(text = stringResource(R.string.popular))
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                        if (screenModel.source.supportsLatest) {
 | 
			
		||||
                            FilterChip(
 | 
			
		||||
                                selected = state.currentFilter == BrowseSourceScreenModel.Filter.Latest,
 | 
			
		||||
                                onClick = {
 | 
			
		||||
                                    screenModel.reset()
 | 
			
		||||
                                    screenModel.search(GetRemoteManga.QUERY_LATEST)
 | 
			
		||||
                                },
 | 
			
		||||
                                leadingIcon = {
 | 
			
		||||
                                    Icon(
 | 
			
		||||
                                        imageVector = Icons.Outlined.NewReleases,
 | 
			
		||||
                                        contentDescription = "",
 | 
			
		||||
                                        modifier = Modifier
 | 
			
		||||
                                            .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                                label = {
 | 
			
		||||
                                    Text(text = stringResource(R.string.latest))
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        if (state.filters.isNotEmpty()) {
 | 
			
		||||
                            FilterChip(
 | 
			
		||||
                                selected = state.currentFilter is BrowseSourceScreenModel.Filter.UserInput,
 | 
			
		||||
                                onClick = screenModel::openFilterSheet,
 | 
			
		||||
                                leadingIcon = {
 | 
			
		||||
                                    Icon(
 | 
			
		||||
                                        imageVector = Icons.Outlined.FilterList,
 | 
			
		||||
                                        contentDescription = "",
 | 
			
		||||
                                        modifier = Modifier
 | 
			
		||||
                                            .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                    )
 | 
			
		||||
                                },
 | 
			
		||||
                                label = {
 | 
			
		||||
                                    Text(text = stringResource(R.string.action_filter))
 | 
			
		||||
                                },
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Divider()
 | 
			
		||||
 | 
			
		||||
                    AppStateBanners(screenModel.isDownloadOnly, screenModel.isIncognitoMode)
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
 | 
			
		||||
        ) { paddingValues ->
 | 
			
		||||
            val mangaList = remember(state.currentFilter) {
 | 
			
		||||
                screenModel.getMangaListFlow(state.currentFilter)
 | 
			
		||||
            }.collectAsLazyPagingItems()
 | 
			
		||||
 | 
			
		||||
            BrowseSourceContent(
 | 
			
		||||
                source = screenModel.source,
 | 
			
		||||
                mangaList = mangaList,
 | 
			
		||||
                columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
 | 
			
		||||
                displayMode = screenModel.displayMode,
 | 
			
		||||
                snackbarHostState = snackbarHostState,
 | 
			
		||||
                contentPadding = paddingValues,
 | 
			
		||||
                onWebViewClick = onWebViewClick,
 | 
			
		||||
                onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
 | 
			
		||||
                onLocalSourceHelpClick = onHelpClick,
 | 
			
		||||
                onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
                onMangaLongClick = { manga ->
 | 
			
		||||
                    scope.launchIO {
 | 
			
		||||
                        val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
 | 
			
		||||
                        when {
 | 
			
		||||
                            manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
 | 
			
		||||
                            duplicateManga != null -> screenModel.setDialog(
 | 
			
		||||
                                BrowseSourceScreenModel.Dialog.AddDuplicateManga(
 | 
			
		||||
                                    manga,
 | 
			
		||||
                                    duplicateManga,
 | 
			
		||||
                                ),
 | 
			
		||||
                            )
 | 
			
		||||
                            else -> screenModel.addFavorite(manga)
 | 
			
		||||
                        }
 | 
			
		||||
                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val onDismissRequest = { screenModel.setDialog(null) }
 | 
			
		||||
        when (val dialog = state.dialog) {
 | 
			
		||||
            is BrowseSourceScreenModel.Dialog.Migrate -> {}
 | 
			
		||||
            is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
 | 
			
		||||
                DuplicateMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = { screenModel.addFavorite(dialog.manga) },
 | 
			
		||||
                    onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
 | 
			
		||||
                    duplicateFrom = screenModel.getSourceOrStub(dialog.duplicate),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourceScreenModel.Dialog.RemoveManga -> {
 | 
			
		||||
                RemoveMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        screenModel.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                    mangaToRemove = dialog.manga,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
 | 
			
		||||
                ChangeCategoryDialog(
 | 
			
		||||
                    initialSelection = dialog.initialSelection,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onEditCategories = {
 | 
			
		||||
                        router.pushController(CategoryController())
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = { include, _ ->
 | 
			
		||||
                        screenModel.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                        screenModel.moveMangaToCategories(dialog.manga, include)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            else -> {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        BackHandler(onBack = navigateUp)
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(state.filters) {
 | 
			
		||||
            screenModel.initFilterSheet(context)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(Unit) {
 | 
			
		||||
            queryEvent.receiveAsFlow()
 | 
			
		||||
                .collectLatest {
 | 
			
		||||
                    when (it) {
 | 
			
		||||
                        is SearchType.Genre -> screenModel.searchGenre(it.txt)
 | 
			
		||||
                        is SearchType.Text -> screenModel.search(it.txt)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val queryEvent = Channel<SearchType>()
 | 
			
		||||
    suspend fun search(query: String) = queryEvent.send(SearchType.Text(query))
 | 
			
		||||
    suspend fun searchGenre(name: String) = queryEvent.send(SearchType.Genre(name))
 | 
			
		||||
 | 
			
		||||
    sealed class SearchType(val txt: String) {
 | 
			
		||||
        class Text(txt: String) : SearchType(txt)
 | 
			
		||||
        class Genre(txt: String) : SearchType(txt)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +1,22 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.Immutable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.produceState
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.platform.LocalConfiguration
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.Pager
 | 
			
		||||
import androidx.paging.PagingConfig
 | 
			
		||||
import androidx.paging.PagingData
 | 
			
		||||
import androidx.paging.cachedIn
 | 
			
		||||
import androidx.paging.map
 | 
			
		||||
import cafe.adriel.voyager.core.model.StateScreenModel
 | 
			
		||||
import cafe.adriel.voyager.core.model.coroutineScope
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.core.prefs.CheckboxState
 | 
			
		||||
import eu.kanade.core.prefs.asState
 | 
			
		||||
import eu.kanade.core.prefs.mapAsCheckboxState
 | 
			
		||||
import eu.kanade.domain.base.BasePreferences
 | 
			
		||||
import eu.kanade.domain.category.interactor.GetCategories
 | 
			
		||||
@@ -39,8 +38,6 @@ import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.domain.source.service.SourcePreferences
 | 
			
		||||
import eu.kanade.domain.track.interactor.InsertTrack
 | 
			
		||||
import eu.kanade.domain.track.model.toDomainTrack
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceState
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceStateImpl
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
@@ -48,9 +45,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
 | 
			
		||||
@@ -70,19 +65,23 @@ import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
 | 
			
		||||
import eu.kanade.tachiyomi.util.removeCovers
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.StateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.filterNotNull
 | 
			
		||||
import kotlinx.coroutines.flow.firstOrNull
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
import kotlinx.coroutines.flow.stateIn
 | 
			
		||||
import kotlinx.coroutines.flow.update
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
 | 
			
		||||
 | 
			
		||||
open class BrowseSourcePresenter(
 | 
			
		||||
class BrowseSourceScreenModel(
 | 
			
		||||
    private val sourceId: Long,
 | 
			
		||||
    searchQuery: String? = null,
 | 
			
		||||
    private val state: BrowseSourceStateImpl = BrowseSourceState(searchQuery) as BrowseSourceStateImpl,
 | 
			
		||||
    searchQuery: String?,
 | 
			
		||||
    private val sourceManager: SourceManager = Injekt.get(),
 | 
			
		||||
    preferences: BasePreferences = Injekt.get(),
 | 
			
		||||
    sourcePreferences: SourcePreferences = Injekt.get(),
 | 
			
		||||
@@ -99,86 +98,122 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    private val updateManga: UpdateManga = Injekt.get(),
 | 
			
		||||
    private val insertTrack: InsertTrack = Injekt.get(),
 | 
			
		||||
    private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
 | 
			
		||||
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
 | 
			
		||||
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Filter.valueOf(searchQuery))) {
 | 
			
		||||
 | 
			
		||||
    private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
 | 
			
		||||
 | 
			
		||||
    var displayMode by sourcePreferences.sourceDisplayMode().asState()
 | 
			
		||||
    var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
 | 
			
		||||
 | 
			
		||||
    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
 | 
			
		||||
    val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
 | 
			
		||||
    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
 | 
			
		||||
    val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
 | 
			
		||||
        val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
 | 
			
		||||
        return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
 | 
			
		||||
            (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns())
 | 
			
		||||
                .changes()
 | 
			
		||||
                .collectLatest { columns ->
 | 
			
		||||
                    value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    val source = sourceManager.get(sourceId) as CatalogueSource
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sheet containing filter items.
 | 
			
		||||
     */
 | 
			
		||||
    private var filterSheet: SourceFilterSheet? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        mutableState.update { it.copy(filters = source.getFilterList()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getMangaList(): Flow<PagingData<Manga>> {
 | 
			
		||||
        return remember(currentFilter) {
 | 
			
		||||
            Pager(
 | 
			
		||||
                PagingConfig(pageSize = 25),
 | 
			
		||||
            ) {
 | 
			
		||||
                getRemoteManga.subscribe(sourceId, currentFilter.query, currentFilter.filters)
 | 
			
		||||
            }.flow
 | 
			
		||||
                .map {
 | 
			
		||||
                    it.map { sManga ->
 | 
			
		||||
                        withIOContext {
 | 
			
		||||
                            networkToLocalManga.await(sManga.toDomainManga(sourceId))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .cachedIn(presenterScope)
 | 
			
		||||
        }
 | 
			
		||||
    fun getColumnsPreference(orientation: Int): GridCells {
 | 
			
		||||
        val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
 | 
			
		||||
        val columns = if (isLandscape) {
 | 
			
		||||
            libraryPreferences.landscapeColumns()
 | 
			
		||||
        } else {
 | 
			
		||||
            libraryPreferences.portraitColumns()
 | 
			
		||||
        }.get()
 | 
			
		||||
        return if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getManga(initialManga: Manga): State<Manga> {
 | 
			
		||||
        return produceState(initialValue = initialManga) {
 | 
			
		||||
            getManga.subscribe(initialManga.url, initialManga.source)
 | 
			
		||||
                .collectLatest { manga ->
 | 
			
		||||
                    if (manga == null) return@collectLatest
 | 
			
		||||
                    withIOContext {
 | 
			
		||||
                        initializeManga(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                    value = manga
 | 
			
		||||
    fun getMangaListFlow(currentFilter: Filter): Flow<PagingData<StateFlow<Manga>>> {
 | 
			
		||||
        return Pager(
 | 
			
		||||
            PagingConfig(pageSize = 25),
 | 
			
		||||
        ) {
 | 
			
		||||
            getRemoteManga.subscribe(sourceId, currentFilter.query ?: "", currentFilter.filters)
 | 
			
		||||
        }.flow
 | 
			
		||||
            .map { pagingData ->
 | 
			
		||||
                pagingData.map { sManga ->
 | 
			
		||||
                    val dbManga = withIOContext { networkToLocalManga.await(sManga.toDomainManga(sourceId)) }
 | 
			
		||||
                    getManga.subscribe(dbManga.url, dbManga.source)
 | 
			
		||||
                        .filterNotNull()
 | 
			
		||||
                        .onEach { initializeManga(it) }
 | 
			
		||||
                        .stateIn(coroutineScope)
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
            }
 | 
			
		||||
            .cachedIn(coroutineScope)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun reset() {
 | 
			
		||||
        val source = source ?: return
 | 
			
		||||
        state.filters = source.getFilterList()
 | 
			
		||||
        mutableState.update { it.copy(filters = source.getFilterList()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search(query: String? = null, filters: FilterList? = null) {
 | 
			
		||||
        Filter.valueOf(query ?: "").let {
 | 
			
		||||
        Filter.valueOf(query).let {
 | 
			
		||||
            if (it !is Filter.UserInput) {
 | 
			
		||||
                state.currentFilter = it
 | 
			
		||||
                state.searchQuery = null
 | 
			
		||||
                mutableState.update { state -> state.copy(currentFilter = it) }
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val input: Filter.UserInput = if (currentFilter is Filter.UserInput) currentFilter as Filter.UserInput else Filter.UserInput()
 | 
			
		||||
        state.currentFilter = input.copy(
 | 
			
		||||
            query = query ?: input.query,
 | 
			
		||||
            filters = filters ?: input.filters,
 | 
			
		||||
        )
 | 
			
		||||
        val input = if (state.value.currentFilter is Filter.UserInput) {
 | 
			
		||||
            state.value.currentFilter as Filter.UserInput
 | 
			
		||||
        } else {
 | 
			
		||||
            Filter.UserInput()
 | 
			
		||||
        }
 | 
			
		||||
        mutableState.update {
 | 
			
		||||
            it.copy(
 | 
			
		||||
                currentFilter = input.copy(
 | 
			
		||||
                    query = query ?: input.query,
 | 
			
		||||
                    filters = filters ?: input.filters,
 | 
			
		||||
                ),
 | 
			
		||||
                toolbarQuery = query ?: input.query,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
    fun searchGenre(genreName: String) {
 | 
			
		||||
        val defaultFilters = source.getFilterList()
 | 
			
		||||
        var genreExists = false
 | 
			
		||||
 | 
			
		||||
        state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
 | 
			
		||||
        state.filters = source!!.getFilterList()
 | 
			
		||||
        filter@ for (sourceFilter in defaultFilters) {
 | 
			
		||||
            if (sourceFilter is SourceModelFilter.Group<*>) {
 | 
			
		||||
                for (filter in sourceFilter.state) {
 | 
			
		||||
                    if (filter is SourceModelFilter<*> && filter.name.equals(genreName, true)) {
 | 
			
		||||
                        when (filter) {
 | 
			
		||||
                            is SourceModelFilter.TriState -> filter.state = 1
 | 
			
		||||
                            is SourceModelFilter.CheckBox -> filter.state = true
 | 
			
		||||
                            else -> {}
 | 
			
		||||
                        }
 | 
			
		||||
                        genreExists = true
 | 
			
		||||
                        break@filter
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else if (sourceFilter is SourceModelFilter.Select<*>) {
 | 
			
		||||
                val index = sourceFilter.values.filterIsInstance<String>()
 | 
			
		||||
                    .indexOfFirst { it.equals(genreName, true) }
 | 
			
		||||
 | 
			
		||||
                if (index != -1) {
 | 
			
		||||
                    sourceFilter.state = index
 | 
			
		||||
                    genreExists = true
 | 
			
		||||
                    break
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mutableState.update {
 | 
			
		||||
            val filter = if (genreExists) {
 | 
			
		||||
                Filter.UserInput(filters = defaultFilters)
 | 
			
		||||
            } else {
 | 
			
		||||
                Filter.UserInput(query = genreName)
 | 
			
		||||
            }
 | 
			
		||||
            it.copy(
 | 
			
		||||
                filters = defaultFilters,
 | 
			
		||||
                currentFilter = filter,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -190,7 +225,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        if (manga.thumbnailUrl != null || manga.initialized) return
 | 
			
		||||
        withNonCancellableContext {
 | 
			
		||||
            try {
 | 
			
		||||
                val networkManga = source!!.getMangaDetails(manga.toSManga())
 | 
			
		||||
                val networkManga = source.getMangaDetails(manga.toSManga())
 | 
			
		||||
                val updatedManga = manga.copyFrom(networkManga)
 | 
			
		||||
                    .copy(initialized = true)
 | 
			
		||||
 | 
			
		||||
@@ -207,7 +242,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
     * @param manga the manga to update.
 | 
			
		||||
     */
 | 
			
		||||
    fun changeMangaFavorite(manga: Manga) {
 | 
			
		||||
        presenterScope.launch {
 | 
			
		||||
        coroutineScope.launch {
 | 
			
		||||
            var new = manga.copy(
 | 
			
		||||
                favorite = !manga.favorite,
 | 
			
		||||
                dateAdded = when (manga.favorite) {
 | 
			
		||||
@@ -233,7 +268,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addFavorite(manga: Manga) {
 | 
			
		||||
        presenterScope.launch {
 | 
			
		||||
        coroutineScope.launch {
 | 
			
		||||
            val categories = getCategories()
 | 
			
		||||
            val defaultCategoryId = libraryPreferences.defaultCategory().get()
 | 
			
		||||
            val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
 | 
			
		||||
@@ -256,7 +291,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
                // Choose a category
 | 
			
		||||
                else -> {
 | 
			
		||||
                    val preselectedIds = getCategories.await(manga.id).map { it.id }
 | 
			
		||||
                    state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds })
 | 
			
		||||
                    setDialog(Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -265,7 +300,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    private suspend fun autoAddTrack(manga: Manga) {
 | 
			
		||||
        loggedServices
 | 
			
		||||
            .filterIsInstance<EnhancedTrackService>()
 | 
			
		||||
            .filter { it.accept(source!!) }
 | 
			
		||||
            .filter { it.accept(source) }
 | 
			
		||||
            .forEach { service ->
 | 
			
		||||
                try {
 | 
			
		||||
                    service.match(manga.toDbManga())?.let { track ->
 | 
			
		||||
@@ -303,7 +338,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
 | 
			
		||||
        presenterScope.launchIO {
 | 
			
		||||
        coroutineScope.launchIO {
 | 
			
		||||
            setMangaCategories.await(
 | 
			
		||||
                mangaId = manga.id,
 | 
			
		||||
                categoryIds = categoryIds.toList(),
 | 
			
		||||
@@ -311,13 +346,43 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sealed class Filter(open val query: String, open val filters: FilterList) {
 | 
			
		||||
    fun openFilterSheet() {
 | 
			
		||||
        filterSheet?.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setDialog(dialog: Dialog?) {
 | 
			
		||||
        mutableState.update { it.copy(dialog = dialog) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setToolbarQuery(query: String?) {
 | 
			
		||||
        mutableState.update { it.copy(toolbarQuery = query) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun initFilterSheet(context: Context) {
 | 
			
		||||
        val state = state.value
 | 
			
		||||
        if (state.filters.isEmpty()) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        filterSheet = SourceFilterSheet(
 | 
			
		||||
            context = context,
 | 
			
		||||
            onFilterClicked = { search(filters = state.filters) },
 | 
			
		||||
            onResetClicked = {
 | 
			
		||||
                reset()
 | 
			
		||||
                filterSheet?.setFilters(state.filterItems)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filterSheet?.setFilters(state.filterItems)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sealed class Filter(open val query: String?, open val filters: FilterList) {
 | 
			
		||||
        object Popular : Filter(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList())
 | 
			
		||||
        object Latest : Filter(query = GetRemoteManga.QUERY_LATEST, filters = FilterList())
 | 
			
		||||
        data class UserInput(override val query: String = "", override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters)
 | 
			
		||||
        data class UserInput(override val query: String? = null, override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters)
 | 
			
		||||
 | 
			
		||||
        companion object {
 | 
			
		||||
            fun valueOf(query: String): Filter {
 | 
			
		||||
            fun valueOf(query: String?): Filter {
 | 
			
		||||
                return when (query) {
 | 
			
		||||
                    GetRemoteManga.QUERY_POPULAR -> Popular
 | 
			
		||||
                    GetRemoteManga.QUERY_LATEST -> Latest
 | 
			
		||||
@@ -336,25 +401,40 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        ) : Dialog()
 | 
			
		||||
        data class Migrate(val newManga: Manga) : Dialog()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Immutable
 | 
			
		||||
    data class State(
 | 
			
		||||
        val currentFilter: Filter,
 | 
			
		||||
        val filters: FilterList = FilterList(),
 | 
			
		||||
        val toolbarQuery: String? = null,
 | 
			
		||||
        val dialog: Dialog? = null,
 | 
			
		||||
    ) {
 | 
			
		||||
        val filterItems = filters.toItems()
 | 
			
		||||
        val isUserQuery = currentFilter is Filter.UserInput && !currentFilter.query.isNullOrEmpty()
 | 
			
		||||
        val searchQuery = when (currentFilter) {
 | 
			
		||||
            is Filter.UserInput -> currentFilter.query
 | 
			
		||||
            Filter.Latest, Filter.Popular -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun FilterList.toItems(): List<IFlexible<*>> {
 | 
			
		||||
private fun FilterList.toItems(): List<IFlexible<*>> {
 | 
			
		||||
    return mapNotNull { filter ->
 | 
			
		||||
        when (filter) {
 | 
			
		||||
            is Filter.Header -> HeaderItem(filter)
 | 
			
		||||
            is Filter.Separator -> SeparatorItem(filter)
 | 
			
		||||
            is Filter.CheckBox -> CheckboxItem(filter)
 | 
			
		||||
            is Filter.TriState -> TriStateItem(filter)
 | 
			
		||||
            is Filter.Text -> TextItem(filter)
 | 
			
		||||
            is Filter.Select<*> -> SelectItem(filter)
 | 
			
		||||
            is Filter.Group<*> -> {
 | 
			
		||||
            is SourceModelFilter.Header -> HeaderItem(filter)
 | 
			
		||||
            is SourceModelFilter.Separator -> SeparatorItem(filter)
 | 
			
		||||
            is SourceModelFilter.CheckBox -> CheckboxItem(filter)
 | 
			
		||||
            is SourceModelFilter.TriState -> TriStateItem(filter)
 | 
			
		||||
            is SourceModelFilter.Text -> TextItem(filter)
 | 
			
		||||
            is SourceModelFilter.Select<*> -> SelectItem(filter)
 | 
			
		||||
            is SourceModelFilter.Group<*> -> {
 | 
			
		||||
                val group = GroupItem(filter)
 | 
			
		||||
                val subItems = filter.state.mapNotNull {
 | 
			
		||||
                    when (it) {
 | 
			
		||||
                        is Filter.CheckBox -> CheckboxSectionItem(it)
 | 
			
		||||
                        is Filter.TriState -> TriStateSectionItem(it)
 | 
			
		||||
                        is Filter.Text -> TextSectionItem(it)
 | 
			
		||||
                        is Filter.Select<*> -> SelectSectionItem(it)
 | 
			
		||||
                        is SourceModelFilter.CheckBox -> CheckboxSectionItem(it)
 | 
			
		||||
                        is SourceModelFilter.TriState -> TriStateSectionItem(it)
 | 
			
		||||
                        is SourceModelFilter.Text -> TextSectionItem(it)
 | 
			
		||||
                        is SourceModelFilter.Select<*> -> SelectSectionItem(it)
 | 
			
		||||
                        else -> null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -362,7 +442,7 @@ fun FilterList.toItems(): List<IFlexible<*>> {
 | 
			
		||||
                group.subItems = subItems
 | 
			
		||||
                group
 | 
			
		||||
            }
 | 
			
		||||
            is Filter.Sort -> {
 | 
			
		||||
            is SourceModelFilter.Sort -> {
 | 
			
		||||
                val group = SortGroup(filter)
 | 
			
		||||
                val subItems = filter.values.map {
 | 
			
		||||
                    SortItem(it, group)
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
@@ -13,12 +12,12 @@ import eu.kanade.tachiyomi.widget.SimpleNavigationView
 | 
			
		||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
 | 
			
		||||
 | 
			
		||||
class SourceFilterSheet(
 | 
			
		||||
    activity: Activity,
 | 
			
		||||
    context: Context,
 | 
			
		||||
    private val onFilterClicked: () -> Unit,
 | 
			
		||||
    private val onResetClicked: () -> Unit,
 | 
			
		||||
) : BaseBottomSheetDialog(activity) {
 | 
			
		||||
) : BaseBottomSheetDialog(context) {
 | 
			
		||||
 | 
			
		||||
    private var filterNavView: FilterNavigationView = FilterNavigationView(activity)
 | 
			
		||||
    private var filterNavView: FilterNavigationView = FilterNavigationView(context)
 | 
			
		||||
 | 
			
		||||
    override fun createView(inflater: LayoutInflater): View {
 | 
			
		||||
        filterNavView.onFilterClicked = {
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ class GlobalSearchScreen(
 | 
			
		||||
                if (!screenModel.incognitoMode.get()) {
 | 
			
		||||
                    screenModel.lastUsedSourceId.set(it.id)
 | 
			
		||||
                }
 | 
			
		||||
                router.pushController(BrowseSourceController(it, state.searchQuery))
 | 
			
		||||
                router.pushController(BrowseSourceController(it.id, state.searchQuery))
 | 
			
		||||
            },
 | 
			
		||||
            onClickItem = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onLongClickItem = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user