mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Merge Latest and Browse into one screen (#7921)
* Merge Latest and Browse into one * Add back Latest button * Change context to IO instead of launching a job * Use loading screen when loading initial page
This commit is contained in:
		@@ -0,0 +1,3 @@
 | 
			
		||||
package eu.kanade.data.source
 | 
			
		||||
 | 
			
		||||
class NoResultsException : Exception()
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
package eu.kanade.data.source
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingState
 | 
			
		||||
import eu.kanade.domain.source.model.SourcePagingSourceType
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withIOContext
 | 
			
		||||
 | 
			
		||||
abstract class SourcePagingSource(
 | 
			
		||||
    protected val source: CatalogueSource,
 | 
			
		||||
) : SourcePagingSourceType() {
 | 
			
		||||
 | 
			
		||||
    abstract suspend fun requestNextPage(currentPage: Int): MangasPage
 | 
			
		||||
 | 
			
		||||
    override suspend fun load(params: LoadParams<Long>): LoadResult<Long, SManga> {
 | 
			
		||||
        val page = params.key ?: 1
 | 
			
		||||
 | 
			
		||||
        val mangasPage = try {
 | 
			
		||||
            withIOContext {
 | 
			
		||||
                requestNextPage(page.toInt())
 | 
			
		||||
                    .takeIf { it.mangas.isNotEmpty() }
 | 
			
		||||
                    ?: throw NoResultsException()
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            return LoadResult.Error(e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return LoadResult.Page(
 | 
			
		||||
            data = mangasPage.mangas,
 | 
			
		||||
            prevKey = null,
 | 
			
		||||
            nextKey = if (mangasPage.hasNextPage) page + 1 else null,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getRefreshKey(state: PagingState<Long, SManga>): Long? {
 | 
			
		||||
        return state.anchorPosition?.let { anchorPosition ->
 | 
			
		||||
            val anchorPage = state.closestPageToPosition(anchorPosition)
 | 
			
		||||
            anchorPage?.prevKey ?: anchorPage?.nextKey
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) {
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        return source.fetchPopularManga(currentPage).awaitSingle()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        return source.fetchLatestUpdates(currentPage).awaitSingle()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,13 @@ package eu.kanade.data.source
 | 
			
		||||
 | 
			
		||||
import eu.kanade.data.DatabaseHandler
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.domain.source.model.SourcePagingSourceType
 | 
			
		||||
import eu.kanade.domain.source.model.SourceWithCount
 | 
			
		||||
import eu.kanade.domain.source.repository.SourceRepository
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
 | 
			
		||||
@@ -49,4 +52,23 @@ class SourceRepositoryImpl(
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun search(
 | 
			
		||||
        sourceId: Long,
 | 
			
		||||
        query: String,
 | 
			
		||||
        filterList: FilterList,
 | 
			
		||||
    ): SourcePagingSourceType {
 | 
			
		||||
        val source = sourceManager.get(sourceId) as CatalogueSource
 | 
			
		||||
        return SourceSearchPagingSource(source, query, filterList)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getPopular(sourceId: Long): SourcePagingSourceType {
 | 
			
		||||
        val source = sourceManager.get(sourceId) as CatalogueSource
 | 
			
		||||
        return SourcePopularPagingSource(source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getLatest(sourceId: Long): SourcePagingSourceType {
 | 
			
		||||
        val source = sourceManager.get(sourceId) as CatalogueSource
 | 
			
		||||
        return SourceLatestPagingSource(source)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.manga.repository.MangaRepository
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetEnabledSources
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
 | 
			
		||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
 | 
			
		||||
@@ -133,6 +134,7 @@ class DomainModule : InjektModule {
 | 
			
		||||
        addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
 | 
			
		||||
        addFactory { GetEnabledSources(get(), get()) }
 | 
			
		||||
        addFactory { GetLanguagesWithSources(get(), get()) }
 | 
			
		||||
        addFactory { GetRemoteManga(get()) }
 | 
			
		||||
        addFactory { GetSourcesWithFavoriteCount(get(), get()) }
 | 
			
		||||
        addFactory { GetSourcesWithNonLibraryManga(get()) }
 | 
			
		||||
        addFactory { SetMigrateSorting(get()) }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
package eu.kanade.domain.source.interactor
 | 
			
		||||
 | 
			
		||||
import eu.kanade.domain.source.model.SourcePagingSourceType
 | 
			
		||||
import eu.kanade.domain.source.repository.SourceRepository
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
 | 
			
		||||
class GetRemoteManga(
 | 
			
		||||
    private val repository: SourceRepository,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    fun subscribe(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType {
 | 
			
		||||
        return when (query) {
 | 
			
		||||
            QUERY_POPULAR -> repository.getPopular(sourceId)
 | 
			
		||||
            QUERY_LATEST -> repository.getLatest(sourceId)
 | 
			
		||||
            else -> repository.search(sourceId, query, filterList)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val QUERY_POPULAR = "eu.kanade.domain.source.interactor.POPULAR"
 | 
			
		||||
        const val QUERY_LATEST = "eu.kanade.domain.source.interactor.LATEST"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package eu.kanade.domain.source.model
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
 | 
			
		||||
typealias SourcePagingSourceType = PagingSource<Long, SManga>
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
package eu.kanade.domain.source.repository
 | 
			
		||||
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.domain.source.model.SourcePagingSourceType
 | 
			
		||||
import eu.kanade.domain.source.model.SourceWithCount
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
 | 
			
		||||
interface SourceRepository {
 | 
			
		||||
@@ -13,4 +15,10 @@ interface SourceRepository {
 | 
			
		||||
    fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
 | 
			
		||||
 | 
			
		||||
    fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>>
 | 
			
		||||
 | 
			
		||||
    fun search(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType
 | 
			
		||||
 | 
			
		||||
    fun getPopular(sourceId: Long): SourcePagingSourceType
 | 
			
		||||
 | 
			
		||||
    fun getLatest(sourceId: Long): SourcePagingSourceType
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
import androidx.paging.compose.collectAsLazyPagingItems
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseLatestToolbar
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseLatestScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = { scrollBehavior ->
 | 
			
		||||
            BrowseLatestToolbar(
 | 
			
		||||
                navigateUp = navigateUp,
 | 
			
		||||
                source = presenter.source!!,
 | 
			
		||||
                displayMode = presenter.displayMode,
 | 
			
		||||
                onDisplayModeChange = { presenter.displayMode = it },
 | 
			
		||||
                onHelpClick = onHelpClick,
 | 
			
		||||
                onWebViewClick = onWebViewClick,
 | 
			
		||||
                scrollBehavior = scrollBehavior,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            source = presenter.source,
 | 
			
		||||
            mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
 | 
			
		||||
            getMangaState = { presenter.getManga(it) },
 | 
			
		||||
            columns = columns,
 | 
			
		||||
            displayMode = presenter.displayMode,
 | 
			
		||||
            snackbarHostState = remember { SnackbarHostState() },
 | 
			
		||||
            contentPadding = paddingValues,
 | 
			
		||||
            onWebViewClick = onWebViewClick,
 | 
			
		||||
            onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
 | 
			
		||||
            onLocalSourceHelpClick = onHelpClick,
 | 
			
		||||
            onMangaClick = onMangaClick,
 | 
			
		||||
            onMangaLongClick = onMangaLongClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,18 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.PaddingValues
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.navigationBarsPadding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.lazy.grid.GridCells
 | 
			
		||||
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.SnackbarDuration
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
@@ -20,22 +28,24 @@ 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.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.EmptyScreen
 | 
			
		||||
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.browse.source.browse.NoResultsException
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
@@ -44,7 +54,6 @@ import eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
fun BrowseSourceScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
@@ -68,7 +77,7 @@ fun BrowseSourceScreen(
 | 
			
		||||
                state = presenter,
 | 
			
		||||
                source = presenter.source!!,
 | 
			
		||||
                displayMode = presenter.displayMode,
 | 
			
		||||
                onDisplayModeChange = onDisplayModeChange,
 | 
			
		||||
                onDisplayModeChange = { presenter.displayMode = it },
 | 
			
		||||
                navigateUp = navigateUp,
 | 
			
		||||
                onWebViewClick = onWebViewClick,
 | 
			
		||||
                onHelpClick = onHelpClick,
 | 
			
		||||
@@ -77,21 +86,17 @@ fun BrowseSourceScreen(
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        floatingActionButton = {
 | 
			
		||||
            if (presenter.filters.isNotEmpty()) {
 | 
			
		||||
                ExtendedFloatingActionButton(
 | 
			
		||||
                    modifier = Modifier.navigationBarsPadding(),
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_filter)) },
 | 
			
		||||
                    icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
 | 
			
		||||
                    onClick = onFabClick,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            BrowseSourceFloatingActionButton(
 | 
			
		||||
                isVisible = presenter.filters.isNotEmpty(),
 | 
			
		||||
                onFabClick = onFabClick,
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        snackbarHost = {
 | 
			
		||||
            SnackbarHost(hostState = snackbarHostState)
 | 
			
		||||
        },
 | 
			
		||||
    ) { paddingValues ->
 | 
			
		||||
        BrowseSourceContent(
 | 
			
		||||
            source = presenter.source,
 | 
			
		||||
            state = presenter,
 | 
			
		||||
            mangaList = mangaList,
 | 
			
		||||
            getMangaState = { presenter.getManga(it) },
 | 
			
		||||
            columns = columns,
 | 
			
		||||
@@ -103,15 +108,93 @@ fun BrowseSourceScreen(
 | 
			
		||||
            onLocalSourceHelpClick = onHelpClick,
 | 
			
		||||
            onMangaClick = onMangaClick,
 | 
			
		||||
            onMangaLongClick = onMangaLongClick,
 | 
			
		||||
            header = {
 | 
			
		||||
                Row(
 | 
			
		||||
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
                ) {
 | 
			
		||||
                    FilterChip(
 | 
			
		||||
                        selected = presenter.currentQuery == GetRemoteManga.QUERY_POPULAR,
 | 
			
		||||
                        onClick = {
 | 
			
		||||
                            presenter.resetFilter()
 | 
			
		||||
                            presenter.search(GetRemoteManga.QUERY_POPULAR)
 | 
			
		||||
                        },
 | 
			
		||||
                        leadingIcon = {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Favorite,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                                modifier = Modifier
 | 
			
		||||
                                    .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                            )
 | 
			
		||||
                        },
 | 
			
		||||
                        label = {
 | 
			
		||||
                            Text(text = stringResource(id = R.string.popular))
 | 
			
		||||
                        },
 | 
			
		||||
                    )
 | 
			
		||||
                    if (presenter.source?.supportsLatest == true) {
 | 
			
		||||
                        FilterChip(
 | 
			
		||||
                            selected = presenter.currentQuery == GetRemoteManga.QUERY_LATEST,
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                presenter.resetFilter()
 | 
			
		||||
                                presenter.search(GetRemoteManga.QUERY_LATEST)
 | 
			
		||||
                            },
 | 
			
		||||
                            leadingIcon = {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = Icons.Outlined.NewReleases,
 | 
			
		||||
                                    contentDescription = "",
 | 
			
		||||
                                    modifier = Modifier
 | 
			
		||||
                                        .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            label = {
 | 
			
		||||
                                Text(text = stringResource(id = R.string.latest))
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    if (presenter.filters.isNotEmpty()) {
 | 
			
		||||
                        FilterChip(
 | 
			
		||||
                            selected = presenter.currentQuery != GetRemoteManga.QUERY_POPULAR && presenter.currentQuery != GetRemoteManga.QUERY_LATEST,
 | 
			
		||||
                            onClick = onFabClick,
 | 
			
		||||
                            leadingIcon = {
 | 
			
		||||
                                Icon(
 | 
			
		||||
                                    imageVector = Icons.Outlined.FilterList,
 | 
			
		||||
                                    contentDescription = "",
 | 
			
		||||
                                    modifier = Modifier
 | 
			
		||||
                                        .size(FilterChipDefaults.IconSize),
 | 
			
		||||
                                )
 | 
			
		||||
                            },
 | 
			
		||||
                            label = {
 | 
			
		||||
                                Text(text = stringResource(id = R.string.action_filter))
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceFloatingActionButton(
 | 
			
		||||
    modifier: Modifier = Modifier.navigationBarsPadding(),
 | 
			
		||||
    isVisible: Boolean,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    AnimatedVisibility(visible = isVisible) {
 | 
			
		||||
        ExtendedFloatingActionButton(
 | 
			
		||||
            modifier = modifier,
 | 
			
		||||
            text = { Text(text = stringResource(id = R.string.action_filter)) },
 | 
			
		||||
            icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
 | 
			
		||||
            onClick = onFabClick,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceContent(
 | 
			
		||||
    source: CatalogueSource?,
 | 
			
		||||
    state: BrowseSourceState,
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    header: (@Composable () -> Unit)? = null,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    snackbarHostState: SnackbarHostState,
 | 
			
		||||
@@ -153,7 +236,7 @@ fun BrowseSourceContent(
 | 
			
		||||
    if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
 | 
			
		||||
        EmptyScreen(
 | 
			
		||||
            message = getErrorMessage(errorState),
 | 
			
		||||
            actions = if (source is LocalSource) {
 | 
			
		||||
            actions = if (state.source is LocalSource) {
 | 
			
		||||
                listOf(
 | 
			
		||||
                    EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
 | 
			
		||||
                )
 | 
			
		||||
@@ -169,6 +252,11 @@ fun BrowseSourceContent(
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
 | 
			
		||||
        LoadingScreen()
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    when (displayMode) {
 | 
			
		||||
        LibraryDisplayMode.ComfortableGrid -> {
 | 
			
		||||
            BrowseSourceComfortableGrid(
 | 
			
		||||
@@ -178,6 +266,7 @@ fun BrowseSourceContent(
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
                header = header,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        LibraryDisplayMode.List -> {
 | 
			
		||||
@@ -187,6 +276,7 @@ fun BrowseSourceContent(
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
                header = header,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        else -> {
 | 
			
		||||
@@ -197,6 +287,7 @@ fun BrowseSourceContent(
 | 
			
		||||
                contentPadding = contentPadding,
 | 
			
		||||
                onMangaClick = onMangaClick,
 | 
			
		||||
                onMangaLongClick = onMangaLongClick,
 | 
			
		||||
                header = header,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
@@ -16,22 +17,31 @@ interface BrowseSourceState {
 | 
			
		||||
    val source: CatalogueSource?
 | 
			
		||||
    var searchQuery: String?
 | 
			
		||||
    val currentQuery: String
 | 
			
		||||
    val isUserQuery: Boolean
 | 
			
		||||
    val filters: FilterList
 | 
			
		||||
    val filterItems: List<IFlexible<*>>
 | 
			
		||||
    val appliedFilters: FilterList
 | 
			
		||||
    val currentFilters: FilterList
 | 
			
		||||
    var dialog: BrowseSourcePresenter.Dialog?
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
 | 
			
		||||
    return BrowseSourceStateImpl(initialQuery)
 | 
			
		||||
    if (initialQuery == GetRemoteManga.QUERY_POPULAR || initialQuery == GetRemoteManga.QUERY_LATEST) {
 | 
			
		||||
        return BrowseSourceStateImpl(initialCurrentQuery = initialQuery)
 | 
			
		||||
    }
 | 
			
		||||
    return BrowseSourceStateImpl(initialQuery = initialQuery)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class BrowseSourceStateImpl(initialQuery: String?) : BrowseSourceState {
 | 
			
		||||
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentQuery: String? = initialQuery) : BrowseSourceState {
 | 
			
		||||
    override var source: CatalogueSource? by mutableStateOf(null)
 | 
			
		||||
    override var searchQuery: String? by mutableStateOf(initialQuery)
 | 
			
		||||
    override var currentQuery: String by mutableStateOf(initialQuery ?: "")
 | 
			
		||||
    override var currentQuery: String by mutableStateOf(initialCurrentQuery ?: "")
 | 
			
		||||
    override val isUserQuery: Boolean by derivedStateOf {
 | 
			
		||||
        currentQuery.isNotEmpty() &&
 | 
			
		||||
            currentQuery != GetRemoteManga.QUERY_POPULAR &&
 | 
			
		||||
            currentQuery != GetRemoteManga.QUERY_LATEST
 | 
			
		||||
    }
 | 
			
		||||
    override var filters: FilterList by mutableStateOf(FilterList())
 | 
			
		||||
    override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
 | 
			
		||||
    override var appliedFilters by mutableStateOf(FilterList())
 | 
			
		||||
    override var currentFilters by mutableStateOf(FilterList())
 | 
			
		||||
    override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,73 @@
 | 
			
		||||
package eu.kanade.presentation.browse
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material3.SnackbarHost
 | 
			
		||||
import androidx.compose.material3.SnackbarHostState
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.glance.LocalContext
 | 
			
		||||
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.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.presentation.browse.components.BrowseSourceSearchToolbar
 | 
			
		||||
import eu.kanade.presentation.components.Scaffold
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.more.MoreController
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourceSearchScreen(
 | 
			
		||||
    presenter: BrowseSourcePresenter,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    onFabClick: () -> Unit,
 | 
			
		||||
    onClickManga: (Manga) -> Unit,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    val columns by presenter.getColumnsPreferenceForCurrentOrientation()
 | 
			
		||||
 | 
			
		||||
    BrowseSourceScreen(
 | 
			
		||||
        presenter = presenter,
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        onDisplayModeChange = { presenter.displayMode = (it) },
 | 
			
		||||
        onFabClick = onFabClick,
 | 
			
		||||
        onMangaClick = onClickManga,
 | 
			
		||||
        onMangaLongClick = onClickManga,
 | 
			
		||||
        onWebViewClick = f@{
 | 
			
		||||
            val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
            val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
            context.startActivity(intent)
 | 
			
		||||
    val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
 | 
			
		||||
 | 
			
		||||
    val snackbarHostState = remember { SnackbarHostState() }
 | 
			
		||||
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
 | 
			
		||||
    val onHelpClick = {
 | 
			
		||||
        uriHandler.openUri(LocalSource.HELP_URL)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Scaffold(
 | 
			
		||||
        topBar = { scrollBehavior ->
 | 
			
		||||
            BrowseSourceSearchToolbar(
 | 
			
		||||
                searchQuery = presenter.searchQuery ?: "",
 | 
			
		||||
                onSearchQueryChanged = { presenter.searchQuery = it },
 | 
			
		||||
                navigateUp = navigateUp,
 | 
			
		||||
                onResetClick = { presenter.searchQuery = "" },
 | 
			
		||||
                onSearchClick = { presenter.search() },
 | 
			
		||||
                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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.domain.source.model.Pin
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.presentation.browse.components.BaseSourceItem
 | 
			
		||||
@@ -45,9 +46,8 @@ import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourcesScreen(
 | 
			
		||||
    presenter: SourcesPresenter,
 | 
			
		||||
    onClickItem: (Source) -> Unit,
 | 
			
		||||
    onClickItem: (Source, String) -> Unit,
 | 
			
		||||
    onClickDisable: (Source) -> Unit,
 | 
			
		||||
    onClickLatest: (Source) -> Unit,
 | 
			
		||||
    onClickPin: (Source) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
@@ -59,7 +59,6 @@ fun SourcesScreen(
 | 
			
		||||
                state = presenter,
 | 
			
		||||
                onClickItem = onClickItem,
 | 
			
		||||
                onClickDisable = onClickDisable,
 | 
			
		||||
                onClickLatest = onClickLatest,
 | 
			
		||||
                onClickPin = onClickPin,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
@@ -78,9 +77,8 @@ fun SourcesScreen(
 | 
			
		||||
@Composable
 | 
			
		||||
fun SourceList(
 | 
			
		||||
    state: SourcesState,
 | 
			
		||||
    onClickItem: (Source) -> Unit,
 | 
			
		||||
    onClickItem: (Source, String) -> Unit,
 | 
			
		||||
    onClickDisable: (Source) -> Unit,
 | 
			
		||||
    onClickLatest: (Source) -> Unit,
 | 
			
		||||
    onClickPin: (Source) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    ScrollbarLazyColumn(
 | 
			
		||||
@@ -113,7 +111,6 @@ fun SourceList(
 | 
			
		||||
                    source = model.source,
 | 
			
		||||
                    onClickItem = onClickItem,
 | 
			
		||||
                    onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
 | 
			
		||||
                    onClickLatest = onClickLatest,
 | 
			
		||||
                    onClickPin = onClickPin,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
@@ -155,19 +152,18 @@ fun SourceHeader(
 | 
			
		||||
fun SourceItem(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    source: Source,
 | 
			
		||||
    onClickItem: (Source) -> Unit,
 | 
			
		||||
    onClickItem: (Source, String) -> Unit,
 | 
			
		||||
    onLongClickItem: (Source) -> Unit,
 | 
			
		||||
    onClickLatest: (Source) -> Unit,
 | 
			
		||||
    onClickPin: (Source) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    BaseSourceItem(
 | 
			
		||||
        modifier = modifier,
 | 
			
		||||
        source = source,
 | 
			
		||||
        onClickItem = { onClickItem(source) },
 | 
			
		||||
        onClickItem = { onClickItem(source, GetRemoteManga.QUERY_POPULAR) },
 | 
			
		||||
        onLongClickItem = { onLongClickItem(source) },
 | 
			
		||||
        action = { source ->
 | 
			
		||||
            if (source.supportsLatest) {
 | 
			
		||||
                TextButton(onClick = { onClickLatest(source) }) {
 | 
			
		||||
                TextButton(onClick = { onClickItem(source, GetRemoteManga.QUERY_LATEST) }) {
 | 
			
		||||
                    Text(
 | 
			
		||||
                        text = stringResource(R.string.latest),
 | 
			
		||||
                        style = LocalTextStyle.current.copy(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,108 +0,0 @@
 | 
			
		||||
package eu.kanade.presentation.browse.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.ViewModule
 | 
			
		||||
import androidx.compose.material.icons.outlined.Check
 | 
			
		||||
import androidx.compose.material.icons.outlined.Help
 | 
			
		||||
import androidx.compose.material.icons.outlined.Public
 | 
			
		||||
import androidx.compose.material.icons.outlined.ViewModule
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.material3.TopAppBarScrollBehavior
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.AppBarActions
 | 
			
		||||
import eu.kanade.presentation.components.DropdownMenu
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseLatestToolbar(
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
    source: CatalogueSource,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    onHelpClick: () -> Unit,
 | 
			
		||||
    onWebViewClick: () -> Unit,
 | 
			
		||||
    scrollBehavior: TopAppBarScrollBehavior,
 | 
			
		||||
) {
 | 
			
		||||
    AppBar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        title = source.name,
 | 
			
		||||
        actions = {
 | 
			
		||||
            var selectingDisplayMode by remember { mutableStateOf(false) }
 | 
			
		||||
            AppBarActions(
 | 
			
		||||
                actions = listOf(
 | 
			
		||||
                    AppBar.Action(
 | 
			
		||||
                        title = stringResource(id = R.string.action_display_mode),
 | 
			
		||||
                        icon = Icons.Filled.ViewModule,
 | 
			
		||||
                        onClick = { selectingDisplayMode = true },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (source is LocalSource) {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = stringResource(id = R.string.label_help),
 | 
			
		||||
                            icon = Icons.Outlined.Help,
 | 
			
		||||
                            onClick = onHelpClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = stringResource(id = R.string.action_web_view),
 | 
			
		||||
                            icon = Icons.Outlined.Public,
 | 
			
		||||
                            onClick = onWebViewClick,
 | 
			
		||||
                        )
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            DropdownMenu(
 | 
			
		||||
                expanded = selectingDisplayMode,
 | 
			
		||||
                onDismissRequest = { selectingDisplayMode = false },
 | 
			
		||||
            ) {
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.ComfortableGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_grid)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.CompactGrid) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                DropdownMenuItem(
 | 
			
		||||
                    text = { Text(text = stringResource(id = R.string.action_display_list)) },
 | 
			
		||||
                    onClick = { onDisplayModeChange(LibraryDisplayMode.List) },
 | 
			
		||||
                    trailingIcon = {
 | 
			
		||||
                        if (displayMode == LibraryDisplayMode.List) {
 | 
			
		||||
                            Icon(
 | 
			
		||||
                                imageVector = Icons.Outlined.Check,
 | 
			
		||||
                                contentDescription = "",
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        scrollBehavior = scrollBehavior,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
fun BrowseSourceComfortableGrid(
 | 
			
		||||
    mangaList: LazyPagingItems<Manga>,
 | 
			
		||||
    getMangaState: @Composable ((Manga) -> State<Manga>),
 | 
			
		||||
    header: (@Composable () -> Unit)? = null,
 | 
			
		||||
    columns: GridCells,
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
@@ -37,12 +38,18 @@ fun BrowseSourceComfortableGrid(
 | 
			
		||||
) {
 | 
			
		||||
    LazyVerticalGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp) + contentPadding,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
        if (header != null) {
 | 
			
		||||
            item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
                header()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
            item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -57,8 +64,8 @@ fun BrowseSourceComfortableGrid(
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
 | 
			
		||||
        if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) {
 | 
			
		||||
            item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -41,13 +41,20 @@ fun BrowseSourceCompactGrid(
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
    header: (@Composable () -> Unit)? = null,
 | 
			
		||||
) {
 | 
			
		||||
    LazyVerticalGrid(
 | 
			
		||||
        columns = columns,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp) + contentPadding,
 | 
			
		||||
        contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
 | 
			
		||||
        horizontalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
        verticalArrangement = Arrangement.spacedBy(8.dp),
 | 
			
		||||
    ) {
 | 
			
		||||
        if (header != null) {
 | 
			
		||||
            item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
                header()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item(span = { GridItemSpan(maxLineSpan) }) {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
 
 | 
			
		||||
@@ -30,10 +30,17 @@ fun BrowseSourceList(
 | 
			
		||||
    contentPadding: PaddingValues,
 | 
			
		||||
    onMangaClick: (Manga) -> Unit,
 | 
			
		||||
    onMangaLongClick: (Manga) -> Unit,
 | 
			
		||||
    header: (@Composable () -> Unit)? = null,
 | 
			
		||||
) {
 | 
			
		||||
    LazyColumn(
 | 
			
		||||
        contentPadding = contentPadding,
 | 
			
		||||
    ) {
 | 
			
		||||
        if (header != null) {
 | 
			
		||||
            item {
 | 
			
		||||
                header()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        item {
 | 
			
		||||
            if (mangaList.loadState.prepend is LoadState.Loading) {
 | 
			
		||||
                BrowseSourceLoadingItem()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.material3.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
@@ -18,8 +17,6 @@ fun BrowseSourceLoadingItem() {
 | 
			
		||||
            .padding(vertical = 16.dp),
 | 
			
		||||
        horizontalArrangement = Arrangement.Center,
 | 
			
		||||
    ) {
 | 
			
		||||
        CircularProgressIndicator(
 | 
			
		||||
            modifier = Modifier.size(64.dp),
 | 
			
		||||
        )
 | 
			
		||||
        CircularProgressIndicator()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,11 +43,12 @@ fun BrowseSourceToolbar(
 | 
			
		||||
) {
 | 
			
		||||
    if (state.searchQuery == null) {
 | 
			
		||||
        BrowseSourceRegularToolbar(
 | 
			
		||||
            source = source,
 | 
			
		||||
            title = if (state.isUserQuery) state.currentQuery else source.name,
 | 
			
		||||
            isLocalSource = source is LocalSource,
 | 
			
		||||
            displayMode = displayMode,
 | 
			
		||||
            onDisplayModeChange = onDisplayModeChange,
 | 
			
		||||
            navigateUp = navigateUp,
 | 
			
		||||
            onSearchClick = { state.searchQuery = "" },
 | 
			
		||||
            onSearchClick = { state.searchQuery = if (state.isUserQuery) state.currentQuery else "" },
 | 
			
		||||
            onWebViewClick = onWebViewClick,
 | 
			
		||||
            onHelpClick = onHelpClick,
 | 
			
		||||
            scrollBehavior = scrollBehavior,
 | 
			
		||||
@@ -56,10 +57,7 @@ fun BrowseSourceToolbar(
 | 
			
		||||
        BrowseSourceSearchToolbar(
 | 
			
		||||
            searchQuery = state.searchQuery!!,
 | 
			
		||||
            onSearchQueryChanged = { state.searchQuery = it },
 | 
			
		||||
            navigateUp = {
 | 
			
		||||
                state.searchQuery = null
 | 
			
		||||
                onSearch()
 | 
			
		||||
            },
 | 
			
		||||
            navigateUp = { state.searchQuery = null },
 | 
			
		||||
            onResetClick = { state.searchQuery = "" },
 | 
			
		||||
            onSearchClick = onSearch,
 | 
			
		||||
            scrollBehavior = scrollBehavior,
 | 
			
		||||
@@ -69,7 +67,8 @@ fun BrowseSourceToolbar(
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun BrowseSourceRegularToolbar(
 | 
			
		||||
    source: CatalogueSource,
 | 
			
		||||
    title: String,
 | 
			
		||||
    isLocalSource: Boolean,
 | 
			
		||||
    displayMode: LibraryDisplayMode,
 | 
			
		||||
    onDisplayModeChange: (LibraryDisplayMode) -> Unit,
 | 
			
		||||
    navigateUp: () -> Unit,
 | 
			
		||||
@@ -80,7 +79,7 @@ fun BrowseSourceRegularToolbar(
 | 
			
		||||
) {
 | 
			
		||||
    AppBar(
 | 
			
		||||
        navigateUp = navigateUp,
 | 
			
		||||
        title = source.name,
 | 
			
		||||
        title = title,
 | 
			
		||||
        actions = {
 | 
			
		||||
            var selectingDisplayMode by remember { mutableStateOf(false) }
 | 
			
		||||
            AppBarActions(
 | 
			
		||||
@@ -95,7 +94,7 @@ fun BrowseSourceRegularToolbar(
 | 
			
		||||
                        icon = Icons.Filled.ViewModule,
 | 
			
		||||
                        onClick = { selectingDisplayMode = true },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (source is LocalSource) {
 | 
			
		||||
                    if (isLocalSource) {
 | 
			
		||||
                        AppBar.Action(
 | 
			
		||||
                            title = stringResource(id = R.string.label_help),
 | 
			
		||||
                            icon = Icons.Outlined.Help,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,4 +3,12 @@ package eu.kanade.tachiyomi.source.model
 | 
			
		||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
 | 
			
		||||
 | 
			
		||||
    constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return list.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.manga.model.Manga
 | 
			
		||||
import eu.kanade.presentation.browse.SourceSearchScreen
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
 | 
			
		||||
 | 
			
		||||
class SourceSearchController(
 | 
			
		||||
@@ -27,17 +29,26 @@ class SourceSearchController(
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        // LocalContext is not a first available to us when we try access it
 | 
			
		||||
        // Decoupling from BrowseSourceController is needed
 | 
			
		||||
        val context = applicationContext!!
 | 
			
		||||
 | 
			
		||||
        SourceSearchScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onFabClick = { filterSheet?.show() },
 | 
			
		||||
            onClickManga = {
 | 
			
		||||
            onMangaClick = {
 | 
			
		||||
                newManga = it
 | 
			
		||||
                val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
 | 
			
		||||
                val dialog = SearchController.MigrationDialog(oldManga, newManga, this)
 | 
			
		||||
                dialog.targetController = searchController
 | 
			
		||||
                dialog.showDialog(router)
 | 
			
		||||
            },
 | 
			
		||||
            onWebViewClick = f@{
 | 
			
		||||
                val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
                val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
                context.startActivity(intent)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(presenter.filters) {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun sourcesTab(
 | 
			
		||||
@@ -36,17 +35,13 @@ fun sourcesTab(
 | 
			
		||||
    content = {
 | 
			
		||||
        SourcesScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            onClickItem = { source ->
 | 
			
		||||
            onClickItem = { source, query ->
 | 
			
		||||
                presenter.onOpenSource(source)
 | 
			
		||||
                router?.pushController(BrowseSourceController(source))
 | 
			
		||||
                router?.pushController(BrowseSourceController(source, query))
 | 
			
		||||
            },
 | 
			
		||||
            onClickDisable = { source ->
 | 
			
		||||
                presenter.toggleSource(source)
 | 
			
		||||
            },
 | 
			
		||||
            onClickLatest = { source ->
 | 
			
		||||
                presenter.onOpenSource(source)
 | 
			
		||||
                router?.pushController(LatestUpdatesController(source))
 | 
			
		||||
            },
 | 
			
		||||
            onClickPin = { source ->
 | 
			
		||||
                presenter.togglePin(source)
 | 
			
		||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import androidx.paging.PagingState
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.withIOContext
 | 
			
		||||
 | 
			
		||||
abstract class BrowsePagingSource : PagingSource<Long, SManga>() {
 | 
			
		||||
 | 
			
		||||
    abstract suspend fun requestNextPage(currentPage: Int): MangasPage
 | 
			
		||||
 | 
			
		||||
    override suspend fun load(params: LoadParams<Long>): LoadResult<Long, SManga> {
 | 
			
		||||
        val page = params.key ?: 1
 | 
			
		||||
 | 
			
		||||
        val mangasPage = try {
 | 
			
		||||
            withIOContext {
 | 
			
		||||
                requestNextPage(page.toInt())
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            return LoadResult.Error(e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return LoadResult.Page(
 | 
			
		||||
            data = mangasPage.mangas,
 | 
			
		||||
            prevKey = null,
 | 
			
		||||
            nextKey = if (mangasPage.hasNextPage) page + 1 else null,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getRefreshKey(state: PagingState<Long, SManga>): Long? {
 | 
			
		||||
        return state.anchorPosition?.let { anchorPosition ->
 | 
			
		||||
            val anchorPage = state.closestPageToPosition(anchorPosition)
 | 
			
		||||
            anchorPage?.prevKey ?: anchorPage?.nextKey
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -41,6 +41,10 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
     */
 | 
			
		||||
    protected var filterSheet: SourceFilterSheet? = null
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
@@ -49,7 +53,6 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
        BrowseSourceScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onDisplayModeChange = { presenter.displayMode = (it) },
 | 
			
		||||
            onFabClick = { filterSheet?.show() },
 | 
			
		||||
            onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onMangaLongClick = { manga ->
 | 
			
		||||
@@ -108,10 +111,6 @@ open class BrowseSourceController(bundle: Bundle) :
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun initFilterSheet() {
 | 
			
		||||
        if (presenter.filters.isEmpty()) {
 | 
			
		||||
            return
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.paging.Pager
 | 
			
		||||
import androidx.paging.PagingConfig
 | 
			
		||||
import androidx.paging.PagingData
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import androidx.paging.cachedIn
 | 
			
		||||
import androidx.paging.map
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
@@ -30,6 +29,7 @@ import eu.kanade.domain.manga.interactor.InsertManga
 | 
			
		||||
import eu.kanade.domain.manga.interactor.UpdateManga
 | 
			
		||||
import eu.kanade.domain.manga.model.toDbManga
 | 
			
		||||
import eu.kanade.domain.manga.model.toMangaUpdate
 | 
			
		||||
import eu.kanade.domain.source.interactor.GetRemoteManga
 | 
			
		||||
import eu.kanade.domain.track.interactor.InsertTrack
 | 
			
		||||
import eu.kanade.domain.track.model.toDomainTrack
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseSourceState
 | 
			
		||||
@@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.collectLatest
 | 
			
		||||
import kotlinx.coroutines.flow.firstOrNull
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.mapNotNull
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
@@ -88,6 +87,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    private val sourceManager: SourceManager = Injekt.get(),
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get(),
 | 
			
		||||
    private val coverCache: CoverCache = Injekt.get(),
 | 
			
		||||
    private val getRemoteManga: GetRemoteManga = Injekt.get(),
 | 
			
		||||
    private val getManga: GetManga = Injekt.get(),
 | 
			
		||||
    private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
 | 
			
		||||
    private val getCategories: GetCategories = Injekt.get(),
 | 
			
		||||
@@ -99,6 +99,8 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
 | 
			
		||||
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
 | 
			
		||||
 | 
			
		||||
    private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
 | 
			
		||||
 | 
			
		||||
    var displayMode by preferences.sourceDisplayMode().asState()
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
@@ -115,11 +117,11 @@ open class BrowseSourcePresenter(
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getMangaList(): Flow<PagingData<DomainManga>> {
 | 
			
		||||
        return remember(currentQuery, appliedFilters) {
 | 
			
		||||
        return remember(currentQuery, currentFilters) {
 | 
			
		||||
            Pager(
 | 
			
		||||
                PagingConfig(pageSize = 25),
 | 
			
		||||
            ) {
 | 
			
		||||
                createPager(currentQuery, appliedFilters)
 | 
			
		||||
                getRemoteManga.subscribe(sourceId, currentQuery, currentFilters)
 | 
			
		||||
            }.flow
 | 
			
		||||
                .map {
 | 
			
		||||
                    it.map {
 | 
			
		||||
@@ -134,12 +136,12 @@ open class BrowseSourcePresenter(
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    fun getManga(initialManga: DomainManga): State<DomainManga> {
 | 
			
		||||
        return produceState(initialValue = initialManga, initialManga.url, initialManga.source) {
 | 
			
		||||
        return produceState(initialValue = initialManga) {
 | 
			
		||||
            getManga.subscribe(initialManga.url, initialManga.source)
 | 
			
		||||
                .collectLatest { manga ->
 | 
			
		||||
                    if (manga == null) return@collectLatest
 | 
			
		||||
                    launchIO {
 | 
			
		||||
                        initializeMangas(manga)
 | 
			
		||||
                    withIOContext {
 | 
			
		||||
                        initializeManga(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                    value = manga
 | 
			
		||||
                }
 | 
			
		||||
@@ -151,31 +153,20 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun resetFilter() {
 | 
			
		||||
        state.appliedFilters = FilterList()
 | 
			
		||||
        val newFilters = source!!.getFilterList()
 | 
			
		||||
        state.filters = newFilters
 | 
			
		||||
        state.currentFilters = state.filters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun search() {
 | 
			
		||||
        state.currentQuery = searchQuery ?: ""
 | 
			
		||||
    fun search(query: String? = null) {
 | 
			
		||||
        state.currentQuery = query ?: searchQuery ?: ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
 | 
			
		||||
        state.filters = source!!.getFilterList()
 | 
			
		||||
 | 
			
		||||
        if (savedState != null) {
 | 
			
		||||
            query = savedState.getString(::query.name, "")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSave(state: Bundle) {
 | 
			
		||||
        state.putString(::query.name, query)
 | 
			
		||||
        super.onSave(state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -205,9 +196,9 @@ open class BrowseSourcePresenter(
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param mangas the list of manga to initialize.
 | 
			
		||||
     * @param manga to initialize.
 | 
			
		||||
     */
 | 
			
		||||
    private suspend fun initializeMangas(manga: DomainManga) {
 | 
			
		||||
    private suspend fun initializeManga(manga: DomainManga) {
 | 
			
		||||
        if (manga.thumbnailUrl != null && manga.initialized) return
 | 
			
		||||
        withContext(NonCancellable) {
 | 
			
		||||
            val db = manga.toDbManga()
 | 
			
		||||
@@ -315,11 +306,7 @@ open class BrowseSourcePresenter(
 | 
			
		||||
     * @param filters a list of active filters.
 | 
			
		||||
     */
 | 
			
		||||
    fun setSourceFilter(filters: FilterList) {
 | 
			
		||||
        state.appliedFilters = filters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
 | 
			
		||||
        return SourceBrowsePagingSource(source!!, query, filters)
 | 
			
		||||
        state.currentFilters = filters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -338,12 +325,6 @@ open class BrowseSourcePresenter(
 | 
			
		||||
        return getDuplicateLibraryManga.await(manga.title, manga.source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Move the given manga to categories.
 | 
			
		||||
     *
 | 
			
		||||
     * @param categories the selected categories.
 | 
			
		||||
     * @param manga the manga to move.
 | 
			
		||||
     */
 | 
			
		||||
    fun moveMangaToCategories(manga: DomainManga, vararg categories: DomainCategory) {
 | 
			
		||||
        moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
class NoResultsException : Exception()
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class SourceBrowsePagingSource(val source: CatalogueSource, val query: String, val filters: FilterList) : BrowsePagingSource() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        val observable = if (query.isBlank() && filters.isEmpty()) {
 | 
			
		||||
            source.fetchPopularManga(currentPage)
 | 
			
		||||
        } else {
 | 
			
		||||
            source.fetchSearchManga(currentPage, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return observable.awaitSingle()
 | 
			
		||||
            .takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesBrowsePagingSource(val source: CatalogueSource) : BrowsePagingSource() {
 | 
			
		||||
 | 
			
		||||
    override suspend fun requestNextPage(currentPage: Int): MangasPage {
 | 
			
		||||
        return source.fetchLatestUpdates(currentPage).awaitSingle()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.rememberCoroutineScope
 | 
			
		||||
import androidx.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.core.os.bundleOf
 | 
			
		||||
import eu.kanade.domain.source.model.Source
 | 
			
		||||
import eu.kanade.presentation.browse.BrowseLatestScreen
 | 
			
		||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
 | 
			
		||||
import eu.kanade.presentation.components.ChangeCategoryDialog
 | 
			
		||||
import eu.kanade.presentation.components.DuplicateMangaDialog
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
 | 
			
		||||
 */
 | 
			
		||||
class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(source: Source) : this(
 | 
			
		||||
        bundleOf(SOURCE_ID_KEY to source.id),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseSourcePresenter {
 | 
			
		||||
        return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val scope = rememberCoroutineScope()
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
 | 
			
		||||
        BrowseLatestScreen(
 | 
			
		||||
            presenter = presenter,
 | 
			
		||||
            navigateUp = { router.popCurrentController() },
 | 
			
		||||
            onMangaClick = { router.pushController(MangaController(it.id, true)) },
 | 
			
		||||
            onMangaLongClick = { manga ->
 | 
			
		||||
                scope.launchIO {
 | 
			
		||||
                    val duplicateManga = presenter.getDuplicateLibraryManga(manga)
 | 
			
		||||
                    when {
 | 
			
		||||
                        manga.favorite -> presenter.dialog = BrowseSourcePresenter.Dialog.RemoveManga(manga)
 | 
			
		||||
                        duplicateManga != null -> presenter.dialog = BrowseSourcePresenter.Dialog.AddDuplicateManga(manga, duplicateManga)
 | 
			
		||||
                        else -> presenter.addFavorite(manga)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            onWebViewClick = f@{
 | 
			
		||||
                val source = presenter.source as? HttpSource ?: return@f
 | 
			
		||||
                val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
 | 
			
		||||
                context.startActivity(intent)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val onDismissRequest = { presenter.dialog = null }
 | 
			
		||||
        when (val dialog = presenter.dialog) {
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.AddDuplicateManga -> {
 | 
			
		||||
                DuplicateMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onOpenManga = {
 | 
			
		||||
                        router.pushController(MangaController(dialog.duplicate.id, true))
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.addFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                    duplicateFrom = presenter.getSourceOrStub(dialog.manga),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.RemoveManga -> {
 | 
			
		||||
                RemoveMangaDialog(
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onConfirm = {
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourcePresenter.Dialog.ChangeMangaCategory -> {
 | 
			
		||||
                ChangeCategoryDialog(
 | 
			
		||||
                    initialSelection = dialog.initialSelection,
 | 
			
		||||
                    onDismissRequest = onDismissRequest,
 | 
			
		||||
                    onEditCategories = {
 | 
			
		||||
                        router.pushController(CategoryController())
 | 
			
		||||
                    },
 | 
			
		||||
                    onConfirm = { include, _ ->
 | 
			
		||||
                        presenter.changeMangaFavorite(dialog.manga)
 | 
			
		||||
                        presenter.moveMangaToCategories(dialog.manga, include)
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            null -> {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun initFilterSheet() {
 | 
			
		||||
        // No-op: we don't allow filtering in latest
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.latest
 | 
			
		||||
 | 
			
		||||
import androidx.paging.PagingSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
 | 
			
		||||
 | 
			
		||||
    override fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
 | 
			
		||||
        return LatestUpdatesBrowsePagingSource(source!!)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -42,7 +42,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.category.CategoryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.main.MainActivity
 | 
			
		||||
@@ -313,10 +312,6 @@ class MangaController : FullComposeController<MangaPresenter> {
 | 
			
		||||
                val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController
 | 
			
		||||
                controller.search(query)
 | 
			
		||||
            }
 | 
			
		||||
            is LatestUpdatesController -> {
 | 
			
		||||
                // Search doesn't currently work in source Latest view
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            is BrowseSourceController -> {
 | 
			
		||||
                router.handleBack()
 | 
			
		||||
                previousController.searchWithQuery(query)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user