mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +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