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:
Andreas 2022-09-03 16:16:30 +02:00 committed by GitHub
parent 5a320d87e8
commit cc6aef693e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 389 additions and 475 deletions

View File

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

View File

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

View File

@ -2,10 +2,13 @@ package eu.kanade.data.source
import eu.kanade.data.DatabaseHandler import eu.kanade.data.DatabaseHandler
import eu.kanade.domain.source.model.Source 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.model.SourceWithCount
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map 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)
}
} }

View File

@ -51,6 +51,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.source.interactor.GetEnabledSources import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources 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.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
@ -133,6 +134,7 @@ class DomainModule : InjektModule {
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) } addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
addFactory { GetEnabledSources(get(), get()) } addFactory { GetEnabledSources(get(), get()) }
addFactory { GetLanguagesWithSources(get(), get()) } addFactory { GetLanguagesWithSources(get(), get()) }
addFactory { GetRemoteManga(get()) }
addFactory { GetSourcesWithFavoriteCount(get(), get()) } addFactory { GetSourcesWithFavoriteCount(get(), get()) }
addFactory { GetSourcesWithNonLibraryManga(get()) } addFactory { GetSourcesWithNonLibraryManga(get()) }
addFactory { SetMigrateSorting(get()) } addFactory { SetMigrateSorting(get()) }

View File

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

View File

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

View File

@ -1,7 +1,9 @@
package eu.kanade.domain.source.repository package eu.kanade.domain.source.repository
import eu.kanade.domain.source.model.Source 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.model.SourceWithCount
import eu.kanade.tachiyomi.source.model.FilterList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SourceRepository { interface SourceRepository {
@ -13,4 +15,10 @@ interface SourceRepository {
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>>
fun search(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType
fun getPopular(sourceId: Long): SourcePagingSourceType
fun getLatest(sourceId: Long): SourcePagingSourceType
} }

View File

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

View File

@ -1,10 +1,18 @@
package eu.kanade.presentation.browse 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons 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.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.Icon
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost 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.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.data.source.NoResultsException
import eu.kanade.domain.manga.model.Manga 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.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.browse.components.BrowseSourceToolbar import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.ExtendedFloatingActionButton import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter 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.library.setting.LibraryDisplayMode
import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.EmptyView
@ -44,7 +54,6 @@ import eu.kanade.tachiyomi.widget.EmptyView
fun BrowseSourceScreen( fun BrowseSourceScreen(
presenter: BrowseSourcePresenter, presenter: BrowseSourcePresenter,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
onFabClick: () -> Unit, onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
@ -68,7 +77,7 @@ fun BrowseSourceScreen(
state = presenter, state = presenter,
source = presenter.source!!, source = presenter.source!!,
displayMode = presenter.displayMode, displayMode = presenter.displayMode,
onDisplayModeChange = onDisplayModeChange, onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp, navigateUp = navigateUp,
onWebViewClick = onWebViewClick, onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick, onHelpClick = onHelpClick,
@ -77,21 +86,17 @@ fun BrowseSourceScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
if (presenter.filters.isNotEmpty()) { BrowseSourceFloatingActionButton(
ExtendedFloatingActionButton( isVisible = presenter.filters.isNotEmpty(),
modifier = Modifier.navigationBarsPadding(), onFabClick = onFabClick,
text = { Text(text = stringResource(id = R.string.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
) )
}
}, },
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) SnackbarHost(hostState = snackbarHostState)
}, },
) { paddingValues -> ) { paddingValues ->
BrowseSourceContent( BrowseSourceContent(
source = presenter.source, state = presenter,
mangaList = mangaList, mangaList = mangaList,
getMangaState = { presenter.getManga(it) }, getMangaState = { presenter.getManga(it) },
columns = columns, columns = columns,
@ -103,15 +108,93 @@ fun BrowseSourceScreen(
onLocalSourceHelpClick = onHelpClick, onLocalSourceHelpClick = onHelpClick,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, 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 @Composable
fun BrowseSourceContent( fun BrowseSourceContent(
source: CatalogueSource?, state: BrowseSourceState,
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>), getMangaState: @Composable ((Manga) -> State<Manga>),
header: (@Composable () -> Unit)? = null,
columns: GridCells, columns: GridCells,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
@ -153,7 +236,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen( EmptyScreen(
message = getErrorMessage(errorState), message = getErrorMessage(errorState),
actions = if (source is LocalSource) { actions = if (state.source is LocalSource) {
listOf( listOf(
EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() }, EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() },
) )
@ -169,6 +252,11 @@ fun BrowseSourceContent(
return return
} }
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen()
return
}
when (displayMode) { when (displayMode) {
LibraryDisplayMode.ComfortableGrid -> { LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid( BrowseSourceComfortableGrid(
@ -178,6 +266,7 @@ fun BrowseSourceContent(
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
header = header,
) )
} }
LibraryDisplayMode.List -> { LibraryDisplayMode.List -> {
@ -187,6 +276,7 @@ fun BrowseSourceContent(
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
header = header,
) )
} }
else -> { else -> {
@ -197,6 +287,7 @@ fun BrowseSourceContent(
contentPadding = contentPadding, contentPadding = contentPadding,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick, onMangaLongClick = onMangaLongClick,
header = header,
) )
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
@ -16,22 +17,31 @@ interface BrowseSourceState {
val source: CatalogueSource? val source: CatalogueSource?
var searchQuery: String? var searchQuery: String?
val currentQuery: String val currentQuery: String
val isUserQuery: Boolean
val filters: FilterList val filters: FilterList
val filterItems: List<IFlexible<*>> val filterItems: List<IFlexible<*>>
val appliedFilters: FilterList val currentFilters: FilterList
var dialog: BrowseSourcePresenter.Dialog? var dialog: BrowseSourcePresenter.Dialog?
} }
fun BrowseSourceState(initialQuery: String?): BrowseSourceState { 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 source: CatalogueSource? by mutableStateOf(null)
override var searchQuery: String? by mutableStateOf(initialQuery) 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 var filters: FilterList by mutableStateOf(FilterList())
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() } 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) override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
} }

View File

@ -1,32 +1,73 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable 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.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.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.more.MoreController
@Composable @Composable
fun SourceSearchScreen( fun SourceSearchScreen(
presenter: BrowseSourcePresenter, presenter: BrowseSourcePresenter,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onFabClick: () -> Unit, onFabClick: () -> Unit,
onClickManga: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
) { ) {
val context = LocalContext.current val columns by presenter.getColumnsPreferenceForCurrentOrientation()
BrowseSourceScreen( val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
presenter = presenter,
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, navigateUp = navigateUp,
onDisplayModeChange = { presenter.displayMode = (it) }, onResetClick = { presenter.searchQuery = "" },
onFabClick = onFabClick, onSearchClick = { presenter.search() },
onMangaClick = onClickManga, scrollBehavior = scrollBehavior,
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)
}, },
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,
) )
} }
}

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.Pin
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.presentation.browse.components.BaseSourceItem
@ -45,9 +46,8 @@ import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
presenter: SourcesPresenter, presenter: SourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -59,7 +59,6 @@ fun SourcesScreen(
state = presenter, state = presenter,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickDisable = onClickDisable, onClickDisable = onClickDisable,
onClickLatest = onClickLatest,
onClickPin = onClickPin, onClickPin = onClickPin,
) )
} }
@ -78,9 +77,8 @@ fun SourcesScreen(
@Composable @Composable
fun SourceList( fun SourceList(
state: SourcesState, state: SourcesState,
onClickItem: (Source) -> Unit, onClickItem: (Source, String) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
ScrollbarLazyColumn( ScrollbarLazyColumn(
@ -113,7 +111,6 @@ fun SourceList(
source = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) }, onLongClickItem = { state.dialog = SourcesPresenter.Dialog(it) },
onClickLatest = onClickLatest,
onClickPin = onClickPin, onClickPin = onClickPin,
) )
} }
@ -155,19 +152,18 @@ fun SourceHeader(
fun SourceItem( fun SourceItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
source: Source, source: Source,
onClickItem: (Source) -> Unit, onClickItem: (Source, String) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
BaseSourceItem( BaseSourceItem(
modifier = modifier, modifier = modifier,
source = source, source = source,
onClickItem = { onClickItem(source) }, onClickItem = { onClickItem(source, GetRemoteManga.QUERY_POPULAR) },
onLongClickItem = { onLongClickItem(source) }, onLongClickItem = { onLongClickItem(source) },
action = { source -> action = { source ->
if (source.supportsLatest) { if (source.supportsLatest) {
TextButton(onClick = { onClickLatest(source) }) { TextButton(onClick = { onClickItem(source, GetRemoteManga.QUERY_LATEST) }) {
Text( Text(
text = stringResource(R.string.latest), text = stringResource(R.string.latest),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(

View File

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

View File

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.R
fun BrowseSourceComfortableGrid( fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems<Manga>, mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>), getMangaState: @Composable ((Manga) -> State<Manga>),
header: (@Composable () -> Unit)? = null,
columns: GridCells, columns: GridCells,
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
@ -37,12 +38,18 @@ fun BrowseSourceComfortableGrid(
) { ) {
LazyVerticalGrid( LazyVerticalGrid(
columns = columns, columns = columns,
contentPadding = PaddingValues(8.dp) + contentPadding, contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
if (header != null) {
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
header()
}
}
if (mangaList.loadState.prepend is LoadState.Loading) { if (mangaList.loadState.prepend is LoadState.Loading) {
item(span = { GridItemSpan(maxLineSpan) }) {
BrowseSourceLoadingItem() 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() BrowseSourceLoadingItem()
} }
} }

View File

@ -41,13 +41,20 @@ fun BrowseSourceCompactGrid(
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
header: (@Composable () -> Unit)? = null,
) { ) {
LazyVerticalGrid( LazyVerticalGrid(
columns = columns, columns = columns,
contentPadding = PaddingValues(8.dp) + contentPadding, contentPadding = PaddingValues(8.dp, 4.dp) + contentPadding,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
if (header != null) {
item(span = { GridItemSpan(maxLineSpan) }) {
header()
}
}
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
if (mangaList.loadState.prepend is LoadState.Loading) { if (mangaList.loadState.prepend is LoadState.Loading) {
BrowseSourceLoadingItem() BrowseSourceLoadingItem()

View File

@ -30,10 +30,17 @@ fun BrowseSourceList(
contentPadding: PaddingValues, contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit, onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit, onMangaLongClick: (Manga) -> Unit,
header: (@Composable () -> Unit)? = null,
) { ) {
LazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (header != null) {
item {
header()
}
}
item { item {
if (mangaList.loadState.prepend is LoadState.Loading) { if (mangaList.loadState.prepend is LoadState.Loading) {
BrowseSourceLoadingItem() BrowseSourceLoadingItem()

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -18,8 +17,6 @@ fun BrowseSourceLoadingItem() {
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
) { ) {
CircularProgressIndicator( CircularProgressIndicator()
modifier = Modifier.size(64.dp),
)
} }
} }

View File

@ -43,11 +43,12 @@ fun BrowseSourceToolbar(
) { ) {
if (state.searchQuery == null) { if (state.searchQuery == null) {
BrowseSourceRegularToolbar( BrowseSourceRegularToolbar(
source = source, title = if (state.isUserQuery) state.currentQuery else source.name,
isLocalSource = source is LocalSource,
displayMode = displayMode, displayMode = displayMode,
onDisplayModeChange = onDisplayModeChange, onDisplayModeChange = onDisplayModeChange,
navigateUp = navigateUp, navigateUp = navigateUp,
onSearchClick = { state.searchQuery = "" }, onSearchClick = { state.searchQuery = if (state.isUserQuery) state.currentQuery else "" },
onWebViewClick = onWebViewClick, onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick, onHelpClick = onHelpClick,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -56,10 +57,7 @@ fun BrowseSourceToolbar(
BrowseSourceSearchToolbar( BrowseSourceSearchToolbar(
searchQuery = state.searchQuery!!, searchQuery = state.searchQuery!!,
onSearchQueryChanged = { state.searchQuery = it }, onSearchQueryChanged = { state.searchQuery = it },
navigateUp = { navigateUp = { state.searchQuery = null },
state.searchQuery = null
onSearch()
},
onResetClick = { state.searchQuery = "" }, onResetClick = { state.searchQuery = "" },
onSearchClick = onSearch, onSearchClick = onSearch,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -69,7 +67,8 @@ fun BrowseSourceToolbar(
@Composable @Composable
fun BrowseSourceRegularToolbar( fun BrowseSourceRegularToolbar(
source: CatalogueSource, title: String,
isLocalSource: Boolean,
displayMode: LibraryDisplayMode, displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit, onDisplayModeChange: (LibraryDisplayMode) -> Unit,
navigateUp: () -> Unit, navigateUp: () -> Unit,
@ -80,7 +79,7 @@ fun BrowseSourceRegularToolbar(
) { ) {
AppBar( AppBar(
navigateUp = navigateUp, navigateUp = navigateUp,
title = source.name, title = title,
actions = { actions = {
var selectingDisplayMode by remember { mutableStateOf(false) } var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions( AppBarActions(
@ -95,7 +94,7 @@ fun BrowseSourceRegularToolbar(
icon = Icons.Filled.ViewModule, icon = Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true }, onClick = { selectingDisplayMode = true },
), ),
if (source is LocalSource) { if (isLocalSource) {
AppBar.Action( AppBar.Action(
title = stringResource(id = R.string.label_help), title = stringResource(id = R.string.label_help),
icon = Icons.Outlined.Help, icon = Icons.Outlined.Help,

View File

@ -3,4 +3,12 @@ package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list { data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList()) 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()
}
} }

View File

@ -7,7 +7,9 @@ import androidx.core.os.bundleOf
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.SourceSearchScreen import eu.kanade.presentation.browse.SourceSearchScreen
import eu.kanade.tachiyomi.source.CatalogueSource 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.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.getSerializableCompat import eu.kanade.tachiyomi.util.system.getSerializableCompat
class SourceSearchController( class SourceSearchController(
@ -27,17 +29,26 @@ class SourceSearchController(
@Composable @Composable
override fun ComposeContent() { 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( SourceSearchScreen(
presenter = presenter, presenter = presenter,
navigateUp = { router.popCurrentController() }, navigateUp = { router.popCurrentController() },
onFabClick = { filterSheet?.show() }, onFabClick = { filterSheet?.show() },
onClickManga = { onMangaClick = {
newManga = it newManga = it
val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController? val searchController = router.backstack.findLast { it.controller.javaClass == SearchController::class.java }?.controller as SearchController?
val dialog = SearchController.MigrationDialog(oldManga, newManga, this) val dialog = SearchController.MigrationDialog(oldManga, newManga, this)
dialog.targetController = searchController dialog.targetController = searchController
dialog.showDialog(router) 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) { LaunchedEffect(presenter.filters) {

View File

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController 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.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
@Composable @Composable
fun sourcesTab( fun sourcesTab(
@ -36,17 +35,13 @@ fun sourcesTab(
content = { content = {
SourcesScreen( SourcesScreen(
presenter = presenter, presenter = presenter,
onClickItem = { source -> onClickItem = { source, query ->
presenter.onOpenSource(source) presenter.onOpenSource(source)
router?.pushController(BrowseSourceController(source)) router?.pushController(BrowseSourceController(source, query))
}, },
onClickDisable = { source -> onClickDisable = { source ->
presenter.toggleSource(source) presenter.toggleSource(source)
}, },
onClickLatest = { source ->
presenter.onOpenSource(source)
router?.pushController(LatestUpdatesController(source))
},
onClickPin = { source -> onClickPin = { source ->
presenter.togglePin(source) presenter.togglePin(source)
}, },

View File

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

View File

@ -41,6 +41,10 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
protected var filterSheet: SourceFilterSheet? = null protected var filterSheet: SourceFilterSheet? = null
override fun createPresenter(): BrowseSourcePresenter {
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
}
@Composable @Composable
override fun ComposeContent() { override fun ComposeContent() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -49,7 +53,6 @@ open class BrowseSourceController(bundle: Bundle) :
BrowseSourceScreen( BrowseSourceScreen(
presenter = presenter, presenter = presenter,
navigateUp = { router.popCurrentController() }, navigateUp = { router.popCurrentController() },
onDisplayModeChange = { presenter.displayMode = (it) },
onFabClick = { filterSheet?.show() }, onFabClick = { filterSheet?.show() },
onMangaClick = { router.pushController(MangaController(it.id, true)) }, onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaLongClick = { manga -> 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() { open fun initFilterSheet() {
if (presenter.filters.isEmpty()) { if (presenter.filters.isEmpty()) {
return return

View File

@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.map import androidx.paging.map
import eu.davidea.flexibleadapter.items.IFlexible 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.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDbManga import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.manga.model.toMangaUpdate 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.interactor.InsertTrack
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.presentation.browse.BrowseSourceState import eu.kanade.presentation.browse.BrowseSourceState
@ -71,7 +71,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import logcat.LogPriority import logcat.LogPriority
@ -88,6 +87,7 @@ open class BrowseSourcePresenter(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val getRemoteManga: GetRemoteManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(), private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
@ -99,6 +99,8 @@ open class BrowseSourcePresenter(
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state { ) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
var displayMode by preferences.sourceDisplayMode().asState() var displayMode by preferences.sourceDisplayMode().asState()
@Composable @Composable
@ -115,11 +117,11 @@ open class BrowseSourcePresenter(
@Composable @Composable
fun getMangaList(): Flow<PagingData<DomainManga>> { fun getMangaList(): Flow<PagingData<DomainManga>> {
return remember(currentQuery, appliedFilters) { return remember(currentQuery, currentFilters) {
Pager( Pager(
PagingConfig(pageSize = 25), PagingConfig(pageSize = 25),
) { ) {
createPager(currentQuery, appliedFilters) getRemoteManga.subscribe(sourceId, currentQuery, currentFilters)
}.flow }.flow
.map { .map {
it.map { it.map {
@ -134,12 +136,12 @@ open class BrowseSourcePresenter(
@Composable @Composable
fun getManga(initialManga: DomainManga): State<DomainManga> { fun getManga(initialManga: DomainManga): State<DomainManga> {
return produceState(initialValue = initialManga, initialManga.url, initialManga.source) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.collectLatest { manga -> .collectLatest { manga ->
if (manga == null) return@collectLatest if (manga == null) return@collectLatest
launchIO { withIOContext {
initializeMangas(manga) initializeManga(manga)
} }
value = manga value = manga
} }
@ -151,31 +153,20 @@ open class BrowseSourcePresenter(
} }
fun resetFilter() { fun resetFilter() {
state.appliedFilters = FilterList()
val newFilters = source!!.getFilterList() val newFilters = source!!.getFilterList()
state.filters = newFilters state.filters = newFilters
state.currentFilters = state.filters
} }
fun search() { fun search(query: String? = null) {
state.currentQuery = searchQuery ?: "" state.currentQuery = query ?: searchQuery ?: ""
} }
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
state.filters = source!!.getFilterList() 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. * 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 if (manga.thumbnailUrl != null && manga.initialized) return
withContext(NonCancellable) { withContext(NonCancellable) {
val db = manga.toDbManga() val db = manga.toDbManga()
@ -315,11 +306,7 @@ open class BrowseSourcePresenter(
* @param filters a list of active filters. * @param filters a list of active filters.
*/ */
fun setSourceFilter(filters: FilterList) { fun setSourceFilter(filters: FilterList) {
state.appliedFilters = filters state.currentFilters = filters
}
open fun createPager(query: String, filters: FilterList): PagingSource<Long, SManga> {
return SourceBrowsePagingSource(source!!, query, filters)
} }
/** /**
@ -338,12 +325,6 @@ open class BrowseSourcePresenter(
return getDuplicateLibraryManga.await(manga.title, manga.source) 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) { fun moveMangaToCategories(manga: DomainManga, vararg categories: DomainCategory) {
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.migration.search.SearchController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController 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.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.main.MainActivity 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 val controller = router.getControllerWithTag(R.id.nav_library.toString()) as LibraryController
controller.search(query) controller.search(query)
} }
is LatestUpdatesController -> {
// Search doesn't currently work in source Latest view
return
}
is BrowseSourceController -> { is BrowseSourceController -> {
router.handleBack() router.handleBack()
previousController.searchWithQuery(query) previousController.searchWithQuery(query)

View File

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