Use Voyager on BrowseSource and SourceSearch screen (#8650)

Some navigation janks will be dealt with when the migration is complete
This commit is contained in:
Ivan Iskandar 2022-12-01 11:05:11 +07:00 committed by GitHub
parent 8eda4df71f
commit 94d1b68598
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 653 additions and 637 deletions

View file

@ -1,213 +1,37 @@
package eu.kanade.presentation.browse
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.rememberScrollState
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.HelpOutline
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import eu.kanade.data.source.NoResultsException
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
import eu.kanade.presentation.browse.components.BrowseSourceList
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.EmptyScreenAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
@Composable
fun BrowseSourceScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
openFilterSheet: () -> Unit,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
incognitoMode: Boolean,
downloadedOnlyMode: Boolean,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
state = presenter,
source = presenter.source,
displayMode = presenter.displayMode,
onDisplayModeChange = { presenter.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { presenter.search(it) },
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Popular,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_POPULAR)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.popular))
},
)
if (presenter.source?.supportsLatest == true) {
FilterChip(
selected = presenter.currentFilter == BrowseSourcePresenter.Filter.Latest,
onClick = {
presenter.reset()
presenter.search(GetRemoteManga.QUERY_LATEST)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.NewReleases,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.latest))
},
)
}
if (presenter.filters.isNotEmpty()) {
FilterChip(
selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
onClick = openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.action_filter))
},
)
}
}
Divider()
AppStateBanners(downloadedOnlyMode, incognitoMode)
}
},
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 = onMangaLongClick,
)
}
}
@Composable
fun BrowseSourceFloatingActionButton(
modifier: Modifier = Modifier.navigationBarsPadding(),
isVisible: Boolean,
onFabClick: () -> Unit,
) {
AnimatedVisibility(visible = isVisible) {
ExtendedFloatingActionButton(
modifier = modifier,
text = { Text(text = stringResource(R.string.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = onFabClick,
)
}
}
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BrowseSourceContent(
state: BrowseSourceState,
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
source: CatalogueSource?,
mangaList: LazyPagingItems<StateFlow<Manga>>,
columns: GridCells,
displayMode: LibraryDisplayMode,
snackbarHostState: SnackbarHostState,
@ -249,7 +73,7 @@ fun BrowseSourceContent(
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
EmptyScreen(
message = getErrorMessage(errorState),
actions = if (state.source is LocalSource) {
actions = if (source is LocalSource) {
listOf(
EmptyScreenAction(
stringResId = R.string.local_source_help_guide,
@ -290,7 +114,6 @@ fun BrowseSourceContent(
LibraryDisplayMode.ComfortableGrid -> {
BrowseSourceComfortableGrid(
mangaList = mangaList,
getMangaState = getMangaState,
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
@ -300,16 +123,14 @@ fun BrowseSourceContent(
LibraryDisplayMode.List -> {
BrowseSourceList(
mangaList = mangaList,
getMangaState = getMangaState,
contentPadding = contentPadding,
onMangaClick = onMangaClick,
onMangaLongClick = onMangaLongClick,
)
}
else -> {
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
BrowseSourceCompactGrid(
mangaList = mangaList,
getMangaState = getMangaState,
columns = columns,
contentPadding = contentPadding,
onMangaClick = onMangaClick,

View file

@ -1,41 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.source.CatalogueSource
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.Filter
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
@Stable
interface BrowseSourceState {
val source: CatalogueSource?
var searchQuery: String?
val currentFilter: Filter
val isUserQuery: Boolean
val filters: FilterList
val filterItems: List<IFlexible<*>>
var dialog: BrowseSourcePresenter.Dialog?
}
fun BrowseSourceState(initialQuery: String?): BrowseSourceState {
return when (val filter = Filter.valueOf(initialQuery ?: "")) {
Filter.Latest, Filter.Popular -> BrowseSourceStateImpl(initialCurrentFilter = filter)
is Filter.UserInput -> BrowseSourceStateImpl(initialQuery = initialQuery, initialCurrentFilter = filter)
}
}
class BrowseSourceStateImpl(initialQuery: String? = null, initialCurrentFilter: Filter) : BrowseSourceState {
override var source: CatalogueSource? by mutableStateOf(null)
override var searchQuery: String? by mutableStateOf(initialQuery)
override var currentFilter: Filter by mutableStateOf(initialCurrentFilter)
override val isUserQuery: Boolean by derivedStateOf { currentFilter is Filter.UserInput && currentFilter.query.isNotEmpty() }
override var filters: FilterList by mutableStateOf(FilterList())
override val filterItems: List<IFlexible<*>> by derivedStateOf { filters.toItems() }
override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null)
}

View file

@ -1,72 +0,0 @@
package eu.kanade.presentation.browse
import androidx.compose.material3.SnackbarHost
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.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.more.MoreController
@Composable
fun SourceSearchScreen(
presenter: BrowseSourcePresenter,
navigateUp: () -> Unit,
onFabClick: () -> Unit,
onMangaClick: (Manga) -> Unit,
onWebViewClick: () -> Unit,
) {
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
val snackbarHostState = remember { SnackbarHostState() }
val uriHandler = LocalUriHandler.current
val onHelpClick = {
uriHandler.openUri(LocalSource.HELP_URL)
}
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = presenter.searchQuery ?: "",
onChangeSearchQuery = { presenter.searchQuery = it },
onClickCloseSearch = navigateUp,
onSearch = { presenter.search(it) },
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,
)
}
}

View file

@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
@ -17,11 +17,11 @@ import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaComfortableGridItem
import eu.kanade.presentation.util.plus
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BrowseSourceComfortableGrid(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
mangaList: LazyPagingItems<StateFlow<Manga>>,
columns: GridCells,
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
@ -40,8 +40,7 @@ fun BrowseSourceComfortableGrid(
}
items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga)
val manga by mangaList[index]?.collectAsState() ?: return@items
BrowseSourceComfortableGridItem(
manga = manga,
onClick = { onMangaClick(manga) },

View file

@ -6,7 +6,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
@ -17,11 +17,11 @@ import eu.kanade.presentation.browse.InLibraryBadge
import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.MangaCompactGridItem
import eu.kanade.presentation.util.plus
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BrowseSourceCompactGrid(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
mangaList: LazyPagingItems<StateFlow<Manga>>,
columns: GridCells,
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
@ -40,8 +40,7 @@ fun BrowseSourceCompactGrid(
}
items(mangaList.itemCount) { index ->
val initialManga = mangaList[index] ?: return@items
val manga by getMangaState(initialManga)
val manga by mangaList[index]?.collectAsState() ?: return@items
BrowseSourceCompactGridItem(
manga = manga,
onClick = { onMangaClick(manga) },

View file

@ -2,7 +2,7 @@ package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
@ -15,11 +15,11 @@ import eu.kanade.presentation.components.CommonMangaItemDefaults
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaListItem
import eu.kanade.presentation.util.plus
import kotlinx.coroutines.flow.StateFlow
@Composable
fun BrowseSourceList(
mangaList: LazyPagingItems<Manga>,
getMangaState: @Composable ((Manga) -> State<Manga>),
mangaList: LazyPagingItems<StateFlow<Manga>>,
contentPadding: PaddingValues,
onMangaClick: (Manga) -> Unit,
onMangaLongClick: (Manga) -> Unit,
@ -33,9 +33,9 @@ fun BrowseSourceList(
}
}
items(mangaList) { initialManga ->
initialManga ?: return@items
val manga by getMangaState(initialManga)
items(mangaList) { mangaflow ->
mangaflow ?: return@items
val manga by mangaflow.collectAsState()
BrowseSourceListItem(
manga = manga,
onClick = { onMangaClick(manga) },

View file

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
@ -27,7 +26,8 @@ import eu.kanade.tachiyomi.source.LocalSource
@Composable
fun BrowseSourceToolbar(
state: BrowseSourceState,
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
source: CatalogueSource?,
displayMode: LibraryDisplayMode,
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
@ -44,8 +44,8 @@ fun BrowseSourceToolbar(
SearchToolbar(
navigateUp = navigateUp,
titleContent = { AppBarTitle(title) },
searchQuery = state.searchQuery,
onChangeSearchQuery = { state.searchQuery = it },
searchQuery = searchQuery,
onChangeSearchQuery = onSearchQueryChange,
onSearch = onSearch,
onClickCloseSearch = navigateUp,
actions = {

View file

@ -2,26 +2,14 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.SourceSearchScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.setRoot
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.util.system.getSerializableCompat
class SourceSearchController(
bundle: Bundle,
) : BrowseSourceController(bundle) {
class SourceSearchController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf(
@ -31,49 +19,16 @@ class SourceSearchController(
),
)
private var oldManga: Manga? = args.getSerializableCompat(MANGA_KEY)
private var oldManga: Manga = args.getSerializableCompat(MANGA_KEY)!!
private val sourceId = args.getLong(SOURCE_ID_KEY)
private val query = args.getString(SEARCH_QUERY_KEY)
@Composable
override fun ComposeContent() {
SourceSearchScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
onFabClick = { filterSheet?.show() },
onMangaClick = {
presenter.dialog = BrowseSourcePresenter.Dialog.Migrate(it)
},
onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
activity?.let { context ->
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
},
)
when (val dialog = presenter.dialog) {
is BrowseSourcePresenter.Dialog.Migrate -> {
MigrateDialog(
oldManga = oldManga!!,
newManga = dialog.newManga,
// TODO: Move screen model down into Dialog when this screen is using Voyager
screenModel = remember { MigrateDialogScreenModel() },
onDismissRequest = { presenter.dialog = null },
onClickTitle = { router.pushController(MangaController(dialog.newManga.id)) },
onPopScreen = {
// TODO: Push to manga screen and remove this and the previous screen when it moves to Voyager
router.setRoot(BrowseController(toExtensions = false), R.id.nav_browse)
router.pushController(MangaController(dialog.newManga.id))
},
)
}
else -> {}
}
LaunchedEffect(presenter.filters) {
initFilterSheet()
}
Navigator(screen = SourceSearchScreen(oldManga, sourceId, query))
}
}
private const val MANGA_KEY = "oldManga"
private const val SOURCE_ID_KEY = "sourceId"
private const val SEARCH_QUERY_KEY = "searchQuery"

View file

@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.components.ExtendedFloatingActionButton
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.base.controller.setRoot
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
data class SourceSearchScreen(
private val oldManga: Manga,
private val sourceId: Long,
private val query: String? = null,
) : Screen {
@Composable
override fun Content() {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = { scrollBehavior ->
SearchToolbar(
searchQuery = state.toolbarQuery ?: "",
onChangeSearchQuery = screenModel::setToolbarQuery,
onClickCloseSearch = navigateUp,
onSearch = { screenModel.search(it) },
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(R.string.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
onClick = screenModel::openFilterSheet,
)
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
val openMigrateDialog: (Manga) -> Unit = {
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(it))
}
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = {
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
},
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) },
onMangaClick = openMigrateDialog,
onMangaLongClick = openMigrateDialog,
)
}
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {
MigrateDialog(
oldManga = oldManga,
newManga = dialog.newManga,
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
onDismissRequest = { screenModel.setDialog(null) },
onClickTitle = { router.pushController(MangaController(dialog.newManga.id)) },
onPopScreen = {
// TODO: Push to manga screen and remove this and the previous screen when it moves to Voyager
router.setRoot(BrowseController(toExtensions = false), R.id.nav_browse)
router.pushController(MangaController(dialog.newManga.id))
},
)
}
else -> {}
}
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context)
}
}
}

View file

@ -49,7 +49,7 @@ fun Screen.sourcesTab(): TabContent {
contentPadding = contentPadding,
onClickItem = { source, query ->
screenModel.onOpenSource(source)
router.pushController(BrowseSourceController(source, query))
router.pushController(BrowseSourceController(source.id, query))
},
onClickPin = screenModel::togglePin,
onLongClickItem = screenModel::showSourceDialog,

View file

@ -1,32 +1,18 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.core.os.bundleOf
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.BrowseSourceScreen
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Dialog
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
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
open class BrowseSourceController(bundle: Bundle) :
FullComposeController<BrowseSourcePresenter>(bundle) {
class BrowseSourceController(bundle: Bundle) : BasicFullComposeController(bundle) {
constructor(sourceId: Long, query: String? = null) : this(
bundleOf(
@ -35,117 +21,27 @@ open class BrowseSourceController(bundle: Bundle) :
),
)
constructor(source: CatalogueSource, query: String? = null) : this(source.id, query)
private val sourceId = args.getLong(SOURCE_ID_KEY)
private val initialQuery = args.getString(SEARCH_QUERY_KEY)
constructor(source: Source, query: String? = null) : this(source.id, query)
/**
* Sheet containing filter items.
*/
protected var filterSheet: SourceFilterSheet? = null
override fun createPresenter(): BrowseSourcePresenter {
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
}
private val queryEvent = Channel<BrowseSourceScreen.SearchType>()
@Composable
override fun ComposeContent() {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
Navigator(screen = BrowseSourceScreen(sourceId = sourceId, query = initialQuery)) { navigator ->
CurrentScreen()
BrowseSourceScreen(
presenter = presenter,
navigateUp = ::navigateUp,
openFilterSheet = { filterSheet?.show() },
onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
when {
manga.favorite -> presenter.dialog = Dialog.RemoveManga(manga)
duplicateManga != null -> presenter.dialog = Dialog.AddDuplicateManga(manga, duplicateManga)
else -> presenter.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
onWebViewClick = f@{
val source = presenter.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
},
incognitoMode = presenter.isIncognitoMode,
downloadedOnlyMode = presenter.isDownloadOnly,
)
val onDismissRequest = { presenter.dialog = null }
when (val dialog = presenter.dialog) {
null -> {}
is Dialog.Migrate -> {}
is Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { presenter.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
)
}
is Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
presenter.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = {
router.pushController(CategoryController())
},
onConfirm = { include, _ ->
presenter.changeMangaFavorite(dialog.manga)
presenter.moveMangaToCategories(dialog.manga, include)
},
)
LaunchedEffect(Unit) {
queryEvent.consumeAsFlow()
.collectLatest {
val screen = (navigator.lastItem as? BrowseSourceScreen)
when (it) {
is BrowseSourceScreen.SearchType.Genre -> screen?.searchGenre(it.txt)
is BrowseSourceScreen.SearchType.Text -> screen?.search(it.txt)
}
}
BackHandler(onBack = ::navigateUp)
LaunchedEffect(presenter.filters) {
initFilterSheet()
}
}
private fun navigateUp() {
when {
!presenter.isUserQuery && presenter.searchQuery != null -> presenter.searchQuery = null
else -> router.popCurrentController()
}
}
open fun initFilterSheet() {
if (presenter.filters.isEmpty()) {
return
}
filterSheet = SourceFilterSheet(
activity!!,
onFilterClicked = {
presenter.search(filters = presenter.filters)
},
onResetClicked = {
presenter.reset()
filterSheet?.setFilters(presenter.filterItems)
},
)
filterSheet?.setFilters(presenter.filterItems)
}
/**
@ -154,7 +50,7 @@ open class BrowseSourceController(bundle: Bundle) :
* @param newQuery the new query.
*/
fun searchWithQuery(newQuery: String) {
presenter.search(newQuery)
viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Text(newQuery)) }
}
/**
@ -165,46 +61,9 @@ open class BrowseSourceController(bundle: Bundle) :
* @param genreName the name of the genre
*/
fun searchWithGenre(genreName: String) {
val defaultFilters = presenter.source!!.getFilterList()
var genreExists = false
filter@ for (sourceFilter in defaultFilters) {
if (sourceFilter is Filter.Group<*>) {
for (filter in sourceFilter.state) {
if (filter is Filter<*> && filter.name.equals(genreName, true)) {
when (filter) {
is Filter.TriState -> filter.state = 1
is Filter.CheckBox -> filter.state = true
else -> {}
}
genreExists = true
break@filter
}
}
} else if (sourceFilter is Filter.Select<*>) {
val index = sourceFilter.values.filterIsInstance<String>()
.indexOfFirst { it.equals(genreName, true) }
if (index != -1) {
sourceFilter.state = index
genreExists = true
break
}
}
}
if (genreExists) {
filterSheet?.setFilters(defaultFilters.toItems())
presenter.search(filters = defaultFilters)
} else {
searchWithQuery(genreName)
}
}
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
const val SEARCH_QUERY_KEY = "searchQuery"
viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Genre(genreName)) }
}
}
private const val SOURCE_ID_KEY = "sourceId"
private const val SEARCH_QUERY_KEY = "searchQuery"

View file

@ -0,0 +1,283 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.ChangeCategoryDialog
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.DuplicateMangaDialog
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
data class BrowseSourceScreen(
private val sourceId: Long,
private val query: String? = null,
) : Screen {
override val key = uniqueScreenKey
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val screenModel = rememberScreenModel { BrowseSourceScreenModel(sourceId = sourceId, searchQuery = query) }
val state by screenModel.state.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
val onWebViewClick = f@{
val source = screenModel.source as? HttpSource ?: return@f
val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name)
context.startActivity(intent)
}
val navigateUp: () -> Unit = {
when {
navigator.canPop -> navigator.pop()
router.backstackSize > 1 -> router.popCurrentController()
}
}
Scaffold(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
BrowseSourceToolbar(
searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery,
source = screenModel.source,
displayMode = screenModel.displayMode,
onDisplayModeChange = { screenModel.displayMode = it },
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSearch = { screenModel.search(it) },
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = state.currentFilter == BrowseSourceScreenModel.Filter.Popular,
onClick = {
screenModel.reset()
screenModel.search(GetRemoteManga.QUERY_POPULAR)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Favorite,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.popular))
},
)
if (screenModel.source.supportsLatest) {
FilterChip(
selected = state.currentFilter == BrowseSourceScreenModel.Filter.Latest,
onClick = {
screenModel.reset()
screenModel.search(GetRemoteManga.QUERY_LATEST)
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.NewReleases,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.latest))
},
)
}
if (state.filters.isNotEmpty()) {
FilterChip(
selected = state.currentFilter is BrowseSourceScreenModel.Filter.UserInput,
onClick = screenModel::openFilterSheet,
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = "",
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(R.string.action_filter))
},
)
}
}
Divider()
AppStateBanners(screenModel.isDownloadOnly, screenModel.isIncognitoMode)
}
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
val mangaList = remember(state.currentFilter) {
screenModel.getMangaListFlow(state.currentFilter)
}.collectAsLazyPagingItems()
BrowseSourceContent(
source = screenModel.source,
mangaList = mangaList,
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
onWebViewClick = onWebViewClick,
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
onLocalSourceHelpClick = onHelpClick,
onMangaClick = { router.pushController(MangaController(it.id, true)) },
onMangaLongClick = { manga ->
scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga,
duplicateManga,
),
)
else -> screenModel.addFavorite(manga)
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
)
}
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
duplicateFrom = screenModel.getSourceOrStub(dialog.duplicate),
)
}
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
RemoveMangaDialog(
onDismissRequest = onDismissRequest,
onConfirm = {
screenModel.changeMangaFavorite(dialog.manga)
},
mangaToRemove = dialog.manga,
)
}
is BrowseSourceScreenModel.Dialog.ChangeMangaCategory -> {
ChangeCategoryDialog(
initialSelection = dialog.initialSelection,
onDismissRequest = onDismissRequest,
onEditCategories = {
router.pushController(CategoryController())
},
onConfirm = { include, _ ->
screenModel.changeMangaFavorite(dialog.manga)
screenModel.moveMangaToCategories(dialog.manga, include)
},
)
}
else -> {}
}
BackHandler(onBack = navigateUp)
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context)
}
LaunchedEffect(Unit) {
queryEvent.receiveAsFlow()
.collectLatest {
when (it) {
is SearchType.Genre -> screenModel.searchGenre(it.txt)
is SearchType.Text -> screenModel.search(it.txt)
}
}
}
}
private val queryEvent = Channel<SearchType>()
suspend fun search(query: String) = queryEvent.send(SearchType.Text(query))
suspend fun searchGenre(name: String) = queryEvent.send(SearchType.Genre(name))
sealed class SearchType(val txt: String) {
class Text(txt: String) : SearchType(txt)
class Genre(txt: String) : SearchType(txt)
}
}

View file

@ -1,23 +1,22 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.asState
import eu.kanade.core.prefs.mapAsCheckboxState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories
@ -39,8 +38,6 @@ import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.browse.BrowseSourceStateImpl
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
@ -48,9 +45,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
@ -70,19 +65,23 @@ import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
open class BrowseSourcePresenter(
class BrowseSourceScreenModel(
private val sourceId: Long,
searchQuery: String? = null,
private val state: BrowseSourceStateImpl = BrowseSourceState(searchQuery) as BrowseSourceStateImpl,
searchQuery: String?,
private val sourceManager: SourceManager = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
sourcePreferences: SourcePreferences = Injekt.get(),
@ -99,86 +98,122 @@ open class BrowseSourcePresenter(
private val updateManga: UpdateManga = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<BrowseSourceController>(), BrowseSourceState by state {
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Filter.valueOf(searchQuery))) {
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
var displayMode by sourcePreferences.sourceDisplayMode().asState()
var displayMode by sourcePreferences.sourceDisplayMode().asState(coroutineScope)
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
@Composable
fun getColumnsPreferenceForCurrentOrientation(): State<GridCells> {
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
return produceState<GridCells>(initialValue = GridCells.Adaptive(128.dp), isLandscape) {
(if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns())
.changes()
.collectLatest { columns ->
value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
}
}
val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Sheet containing filter items.
*/
private var filterSheet: SourceFilterSheet? = null
init {
mutableState.update { it.copy(filters = source.getFilterList()) }
}
@Composable
fun getMangaList(): Flow<PagingData<Manga>> {
return remember(currentFilter) {
Pager(
fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
val columns = if (isLandscape) {
libraryPreferences.landscapeColumns()
} else {
libraryPreferences.portraitColumns()
}.get()
return if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns)
}
fun getMangaListFlow(currentFilter: Filter): Flow<PagingData<StateFlow<Manga>>> {
return Pager(
PagingConfig(pageSize = 25),
) {
getRemoteManga.subscribe(sourceId, currentFilter.query, currentFilter.filters)
getRemoteManga.subscribe(sourceId, currentFilter.query ?: "", currentFilter.filters)
}.flow
.map {
it.map { sManga ->
withIOContext {
networkToLocalManga.await(sManga.toDomainManga(sourceId))
}
}
}
.cachedIn(presenterScope)
}
}
@Composable
fun getManga(initialManga: Manga): State<Manga> {
return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source)
.collectLatest { manga ->
if (manga == null) return@collectLatest
withIOContext {
initializeManga(manga)
}
value = manga
.map { pagingData ->
pagingData.map { sManga ->
val dbManga = withIOContext { networkToLocalManga.await(sManga.toDomainManga(sourceId)) }
getManga.subscribe(dbManga.url, dbManga.source)
.filterNotNull()
.onEach { initializeManga(it) }
.stateIn(coroutineScope)
}
}
.cachedIn(coroutineScope)
}
fun reset() {
val source = source ?: return
state.filters = source.getFilterList()
mutableState.update { it.copy(filters = source.getFilterList()) }
}
fun search(query: String? = null, filters: FilterList? = null) {
Filter.valueOf(query ?: "").let {
Filter.valueOf(query).let {
if (it !is Filter.UserInput) {
state.currentFilter = it
state.searchQuery = null
mutableState.update { state -> state.copy(currentFilter = it) }
return
}
}
val input: Filter.UserInput = if (currentFilter is Filter.UserInput) currentFilter as Filter.UserInput else Filter.UserInput()
state.currentFilter = input.copy(
val input = if (state.value.currentFilter is Filter.UserInput) {
state.value.currentFilter as Filter.UserInput
} else {
Filter.UserInput()
}
mutableState.update {
it.copy(
currentFilter = input.copy(
query = query ?: input.query,
filters = filters ?: input.filters,
),
toolbarQuery = query ?: input.query,
)
}
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fun searchGenre(genreName: String) {
val defaultFilters = source.getFilterList()
var genreExists = false
state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
state.filters = source!!.getFilterList()
filter@ for (sourceFilter in defaultFilters) {
if (sourceFilter is SourceModelFilter.Group<*>) {
for (filter in sourceFilter.state) {
if (filter is SourceModelFilter<*> && filter.name.equals(genreName, true)) {
when (filter) {
is SourceModelFilter.TriState -> filter.state = 1
is SourceModelFilter.CheckBox -> filter.state = true
else -> {}
}
genreExists = true
break@filter
}
}
} else if (sourceFilter is SourceModelFilter.Select<*>) {
val index = sourceFilter.values.filterIsInstance<String>()
.indexOfFirst { it.equals(genreName, true) }
if (index != -1) {
sourceFilter.state = index
genreExists = true
break
}
}
}
mutableState.update {
val filter = if (genreExists) {
Filter.UserInput(filters = defaultFilters)
} else {
Filter.UserInput(query = genreName)
}
it.copy(
filters = defaultFilters,
currentFilter = filter,
)
}
}
/**
@ -190,7 +225,7 @@ open class BrowseSourcePresenter(
if (manga.thumbnailUrl != null || manga.initialized) return
withNonCancellableContext {
try {
val networkManga = source!!.getMangaDetails(manga.toSManga())
val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.copyFrom(networkManga)
.copy(initialized = true)
@ -207,7 +242,7 @@ open class BrowseSourcePresenter(
* @param manga the manga to update.
*/
fun changeMangaFavorite(manga: Manga) {
presenterScope.launch {
coroutineScope.launch {
var new = manga.copy(
favorite = !manga.favorite,
dateAdded = when (manga.favorite) {
@ -233,7 +268,7 @@ open class BrowseSourcePresenter(
}
fun addFavorite(manga: Manga) {
presenterScope.launch {
coroutineScope.launch {
val categories = getCategories()
val defaultCategoryId = libraryPreferences.defaultCategory().get()
val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() }
@ -256,7 +291,7 @@ open class BrowseSourcePresenter(
// Choose a category
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds })
setDialog(Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }))
}
}
}
@ -265,7 +300,7 @@ open class BrowseSourcePresenter(
private suspend fun autoAddTrack(manga: Manga) {
loggedServices
.filterIsInstance<EnhancedTrackService>()
.filter { it.accept(source!!) }
.filter { it.accept(source) }
.forEach { service ->
try {
service.match(manga.toDbManga())?.let { track ->
@ -303,7 +338,7 @@ open class BrowseSourcePresenter(
}
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
presenterScope.launchIO {
coroutineScope.launchIO {
setMangaCategories.await(
mangaId = manga.id,
categoryIds = categoryIds.toList(),
@ -311,13 +346,43 @@ open class BrowseSourcePresenter(
}
}
sealed class Filter(open val query: String, open val filters: FilterList) {
fun openFilterSheet() {
filterSheet?.show()
}
fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) }
}
fun setToolbarQuery(query: String?) {
mutableState.update { it.copy(toolbarQuery = query) }
}
fun initFilterSheet(context: Context) {
val state = state.value
if (state.filters.isEmpty()) {
return
}
filterSheet = SourceFilterSheet(
context = context,
onFilterClicked = { search(filters = state.filters) },
onResetClicked = {
reset()
filterSheet?.setFilters(state.filterItems)
},
)
filterSheet?.setFilters(state.filterItems)
}
sealed class Filter(open val query: String?, open val filters: FilterList) {
object Popular : Filter(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList())
object Latest : Filter(query = GetRemoteManga.QUERY_LATEST, filters = FilterList())
data class UserInput(override val query: String = "", override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters)
data class UserInput(override val query: String? = null, override val filters: FilterList = FilterList()) : Filter(query = query, filters = filters)
companion object {
fun valueOf(query: String): Filter {
fun valueOf(query: String?): Filter {
return when (query) {
GetRemoteManga.QUERY_POPULAR -> Popular
GetRemoteManga.QUERY_LATEST -> Latest
@ -336,25 +401,40 @@ open class BrowseSourcePresenter(
) : Dialog()
data class Migrate(val newManga: Manga) : Dialog()
}
@Immutable
data class State(
val currentFilter: Filter,
val filters: FilterList = FilterList(),
val toolbarQuery: String? = null,
val dialog: Dialog? = null,
) {
val filterItems = filters.toItems()
val isUserQuery = currentFilter is Filter.UserInput && !currentFilter.query.isNullOrEmpty()
val searchQuery = when (currentFilter) {
is Filter.UserInput -> currentFilter.query
Filter.Latest, Filter.Popular -> null
}
}
}
fun FilterList.toItems(): List<IFlexible<*>> {
private fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull { filter ->
when (filter) {
is Filter.Header -> HeaderItem(filter)
is Filter.Separator -> SeparatorItem(filter)
is Filter.CheckBox -> CheckboxItem(filter)
is Filter.TriState -> TriStateItem(filter)
is Filter.Text -> TextItem(filter)
is Filter.Select<*> -> SelectItem(filter)
is Filter.Group<*> -> {
is SourceModelFilter.Header -> HeaderItem(filter)
is SourceModelFilter.Separator -> SeparatorItem(filter)
is SourceModelFilter.CheckBox -> CheckboxItem(filter)
is SourceModelFilter.TriState -> TriStateItem(filter)
is SourceModelFilter.Text -> TextItem(filter)
is SourceModelFilter.Select<*> -> SelectItem(filter)
is SourceModelFilter.Group<*> -> {
val group = GroupItem(filter)
val subItems = filter.state.mapNotNull {
when (it) {
is Filter.CheckBox -> CheckboxSectionItem(it)
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
is SourceModelFilter.CheckBox -> CheckboxSectionItem(it)
is SourceModelFilter.TriState -> TriStateSectionItem(it)
is SourceModelFilter.Text -> TextSectionItem(it)
is SourceModelFilter.Select<*> -> SelectSectionItem(it)
else -> null
}
}
@ -362,7 +442,7 @@ fun FilterList.toItems(): List<IFlexible<*>> {
group.subItems = subItems
group
}
is Filter.Sort -> {
is SourceModelFilter.Sort -> {
val group = SortGroup(filter)
val subItems = filter.values.map {
SortItem(it, group)

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
@ -13,12 +12,12 @@ import eu.kanade.tachiyomi.widget.SimpleNavigationView
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
class SourceFilterSheet(
activity: Activity,
context: Context,
private val onFilterClicked: () -> Unit,
private val onResetClicked: () -> Unit,
) : BaseBottomSheetDialog(activity) {
) : BaseBottomSheetDialog(context) {
private var filterNavView: FilterNavigationView = FilterNavigationView(activity)
private var filterNavView: FilterNavigationView = FilterNavigationView(context)
override fun createView(inflater: LayoutInflater): View {
filterNavView.onFilterClicked = {

View file

@ -44,7 +44,7 @@ class GlobalSearchScreen(
if (!screenModel.incognitoMode.get()) {
screenModel.lastUsedSourceId.set(it.id)
}
router.pushController(BrowseSourceController(it, state.searchQuery))
router.pushController(BrowseSourceController(it.id, state.searchQuery))
},
onClickItem = { router.pushController(MangaController(it.id, true)) },
onLongClickItem = { router.pushController(MangaController(it.id, true)) },