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:
parent
8eda4df71f
commit
94d1b68598
15 changed files with 653 additions and 637 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) },
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
getRemoteManga.subscribe(sourceId, currentFilter.query, currentFilter.filters)
|
||||
}.flow
|
||||
.map {
|
||||
it.map { sManga ->
|
||||
withIOContext {
|
||||
networkToLocalManga.await(sManga.toDomainManga(sourceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.cachedIn(presenterScope)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@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
|
||||
fun getMangaListFlow(currentFilter: Filter): Flow<PagingData<StateFlow<Manga>>> {
|
||||
return Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
getRemoteManga.subscribe(sourceId, currentFilter.query ?: "", currentFilter.filters)
|
||||
}.flow
|
||||
.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(
|
||||
query = query ?: input.query,
|
||||
filters = filters ?: input.filters,
|
||||
)
|
||||
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)
|
|
@ -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 = {
|
||||
|
|
|
@ -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)) },
|
||||
|
|
Reference in a new issue