浏览代码

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
Andreas 2 年之前
父节点
当前提交
cc6aef693e
共有 31 个文件被更改,包括 389 次插入475 次删除
  1. 3 0
      app/src/main/java/eu/kanade/data/source/NoResultsException.kt
  2. 62 0
      app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt
  3. 22 0
      app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt
  4. 2 0
      app/src/main/java/eu/kanade/domain/DomainModule.kt
  5. 23 0
      app/src/main/java/eu/kanade/domain/source/interactor/GetRemoteManga.kt
  6. 6 0
      app/src/main/java/eu/kanade/domain/source/model/SourcePagingSourceType.kt
  7. 8 0
      app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt
  8. 0 60
      app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt
  9. 106 15
      app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt
  10. 15 5
      app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt
  11. 58 17
      app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt
  12. 6 10
      app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
  13. 0 108
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt
  14. 12 5
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt
  15. 8 1
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt
  16. 7 0
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt
  17. 1 4
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt
  18. 8 9
      app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt
  19. 8 0
      app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt
  20. 12 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
  21. 2 7
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
  22. 0 37
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt
  23. 4 5
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
  24. 15 34
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
  25. 0 3
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/NoResultsException.kt
  26. 0 20
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt
  27. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt
  28. 0 103
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt
  29. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt
  30. 0 5
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  31. 1 0
      app/src/main/res/values/strings.xml

+ 3 - 0
app/src/main/java/eu/kanade/data/source/NoResultsException.kt

@@ -0,0 +1,3 @@
+package eu.kanade.data.source
+
+class NoResultsException : Exception()

+ 62 - 0
app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt

@@ -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()
+    }
+}

+ 22 - 0
app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt

@@ -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)
+    }
 }

+ 2 - 0
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -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()) }

+ 23 - 0
app/src/main/java/eu/kanade/domain/source/interactor/GetRemoteManga.kt

@@ -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"
+    }
+}

+ 6 - 0
app/src/main/java/eu/kanade/domain/source/model/SourcePagingSourceType.kt

@@ -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>

+ 8 - 0
app/src/main/java/eu/kanade/domain/source/repository/SourceRepository.kt

@@ -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
 }

+ 0 - 60
app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt

@@ -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,
-        )
-    }
-}

+ 106 - 15
app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt

@@ -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,
             )
         }
     }

+ 15 - 5
app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt

@@ -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)
 }

+ 58 - 17
app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt

@@ -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,
+        )
+    }
 }

+ 6 - 10
app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt

@@ -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(

+ 0 - 108
app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt

@@ -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,
-    )
-}

+ 12 - 5
app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt

@@ -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()
             }
         }

+ 8 - 1
app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt

@@ -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()

+ 7 - 0
app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt

@@ -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()

+ 1 - 4
app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt

@@ -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()
     }
 }

+ 8 - 9
app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt

@@ -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,

+ 8 - 0
app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt

@@ -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()
+    }
 }

+ 12 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt

@@ -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) {

+ 2 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt

@@ -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)
             },

+ 0 - 37
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt

@@ -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
-        }
-    }
-}

+ 4 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt

@@ -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

+ 15 - 34
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt

@@ -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 })
     }

+ 0 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/NoResultsException.kt

@@ -1,3 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.browse
-
-class NoResultsException : Exception()

+ 0 - 20
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt

@@ -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()
-    }
-}

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt

@@ -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()
-    }
-}

+ 0 - 103
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt

@@ -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
-    }
-}

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt

@@ -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!!)
-    }
-}

+ 0 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -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)

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -582,6 +582,7 @@
     <string name="action_global_search_hint">Global search…</string>
     <string name="action_global_search_query">Search for \"%1$s\" globally</string>
     <string name="latest">Latest</string>
+    <string name="popular">Popular</string>
     <string name="browse">Browse</string>
     <string name="local_source_help_guide">Local source guide</string>
     <string name="no_pinned_sources">You have no pinned sources</string>