mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-29 10:23:21 -05: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:
parent
5a320d87e8
commit
cc6aef693e
31 changed files with 389 additions and 475 deletions
|
@ -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.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) }
|
||||||
|
|
|
@ -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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
navigateUp = navigateUp,
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
onDisplayModeChange = { presenter.displayMode = (it) },
|
|
||||||
onFabClick = onFabClick,
|
val uriHandler = LocalUriHandler.current
|
||||||
onMangaClick = onClickManga,
|
|
||||||
onMangaLongClick = onClickManga,
|
val onHelpClick = {
|
||||||
onWebViewClick = f@{
|
uriHandler.openUri(LocalSource.HELP_URL)
|
||||||
val source = presenter.source as? HttpSource ?: return@f
|
}
|
||||||
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
|
|
||||||
context.startActivity(intent)
|
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.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(
|
||||||
|
|
|
@ -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(
|
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),
|
||||||
) {
|
) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
if (header != null) {
|
||||||
if (mangaList.loadState.prepend is LoadState.Loading) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
header()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue