Use Voyager on BrowseSource and SourceSearch screen (#8650)
Some navigation janks will be dealt with when the migration is complete (cherry picked from commit 94d1b68598692cc0ef981e2dfbf12303fa962f63) # Conflicts: # app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt # app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt # app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt # app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt # app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
This commit is contained in:
parent
6185c95715
commit
6402258c83
@ -22,7 +22,7 @@ class MangaMetadataRepositoryImpl(
|
||||
return handler.awaitOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
|
||||
}
|
||||
|
||||
override suspend fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> {
|
||||
override fun subscribeMetadataById(id: Long): Flow<SearchMetadata?> {
|
||||
return handler.subscribeToOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) }
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ class MangaMetadataRepositoryImpl(
|
||||
return handler.awaitList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
|
||||
}
|
||||
|
||||
override suspend fun subscribeTagsById(id: Long): Flow<List<SearchTag>> {
|
||||
override fun subscribeTagsById(id: Long): Flow<List<SearchTag>> {
|
||||
return handler.subscribeToList { search_tagsQueries.selectByMangaId(id, searchTagMapper) }
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ class MangaMetadataRepositoryImpl(
|
||||
return handler.awaitList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
|
||||
}
|
||||
|
||||
override suspend fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> {
|
||||
override fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>> {
|
||||
return handler.subscribeToList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) }
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ class GetFlatMetadataById(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun subscribe(id: Long): Flow<FlatMetadata?> {
|
||||
fun subscribe(id: Long): Flow<FlatMetadata?> {
|
||||
return combine(
|
||||
mangaMetadataRepository.subscribeMetadataById(id),
|
||||
mangaMetadataRepository.subscribeTagsById(id),
|
||||
|
@ -11,15 +11,15 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface MangaMetadataRepository {
|
||||
suspend fun getMetadataById(id: Long): SearchMetadata?
|
||||
|
||||
suspend fun subscribeMetadataById(id: Long): Flow<SearchMetadata?>
|
||||
fun subscribeMetadataById(id: Long): Flow<SearchMetadata?>
|
||||
|
||||
suspend fun getTagsById(id: Long): List<SearchTag>
|
||||
|
||||
suspend fun subscribeTagsById(id: Long): Flow<List<SearchTag>>
|
||||
fun subscribeTagsById(id: Long): Flow<List<SearchTag>>
|
||||
|
||||
suspend fun getTitlesById(id: Long): List<SearchTitle>
|
||||
|
||||
suspend fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>>
|
||||
fun subscribeTitlesById(id: Long): Flow<List<SearchTitle>>
|
||||
|
||||
suspend fun insertFlatMetadata(flatMetadata: FlatMetadata)
|
||||
|
||||
|
@ -1,68 +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.res.stringResource
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
|
||||
@Composable
|
||||
fun BrowseMangadexFollowsScreen(
|
||||
presenter: BrowseSourcePresenter,
|
||||
navigateUp: () -> Unit,
|
||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
) {
|
||||
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
|
||||
|
||||
val mangaList = presenter.getMangaList().collectAsLazyPagingItems()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
BrowseSourceSimpleToolbar(
|
||||
title = stringResource(R.string.mangadex_follows),
|
||||
displayMode = presenter.displayMode,
|
||||
onDisplayModeChange = onDisplayModeChange,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
BrowseSourceContent(
|
||||
state = presenter,
|
||||
mangaList = mangaList,
|
||||
getMangaState = { presenter.getManga(it) },
|
||||
// SY -->
|
||||
getMetadataState = { manga, metadata ->
|
||||
presenter.getRaisedSearchMetadata(manga, metadata)
|
||||
},
|
||||
// SY <--
|
||||
columns = columns,
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
displayMode = presenter.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = null,
|
||||
onHelpClick = null,
|
||||
onLocalSourceHelpClick = null,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
|
||||
@Composable
|
||||
fun BrowseRecommendationsScreen(
|
||||
presenter: BrowseSourcePresenter,
|
||||
navigateUp: () -> Unit,
|
||||
title: String,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
) {
|
||||
val columns by presenter.getColumnsPreferenceForCurrentOrientation()
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
BrowseSourceSimpleToolbar(
|
||||
navigateUp = navigateUp,
|
||||
title = title,
|
||||
displayMode = presenter.displayMode,
|
||||
onDisplayModeChange = { presenter.displayMode = it },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
BrowseSourceContent(
|
||||
state = presenter,
|
||||
mangaList = presenter.getMangaList().collectAsLazyPagingItems(),
|
||||
getMangaState = { presenter.getManga(it) },
|
||||
// SY -->
|
||||
getMetadataState = { manga, metadata ->
|
||||
presenter.getRaisedSearchMetadata(manga, metadata)
|
||||
},
|
||||
// SY <--
|
||||
columns = columns,
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = false,
|
||||
// SY <--
|
||||
displayMode = presenter.displayMode,
|
||||
snackbarHostState = remember { SnackbarHostState() },
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = null,
|
||||
onHelpClick = null,
|
||||
onLocalSourceHelpClick = null,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaClick,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,250 +1,44 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
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.BrowseSourceEHentaiList
|
||||
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
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.isEhBasedSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceScreen(
|
||||
presenter: BrowseSourcePresenter,
|
||||
navigateUp: () -> Unit,
|
||||
openFilterSheet: () -> Unit,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
// SY -->
|
||||
onSettingsClick: () -> Unit,
|
||||
// SY <--
|
||||
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.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode },
|
||||
onDisplayModeChange = { presenter.displayMode = it },
|
||||
navigateUp = navigateUp,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = onHelpClick,
|
||||
onSearch = { presenter.search(it) },
|
||||
// SY -->
|
||||
onSettingsClick = onSettingsClick,
|
||||
// SY <--
|
||||
)
|
||||
|
||||
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))
|
||||
},
|
||||
)
|
||||
}
|
||||
/* SY --> if (presenter.filters.isNotEmpty())*/ run /* SY <-- */ {
|
||||
FilterChip(
|
||||
selected = presenter.currentFilter is BrowseSourcePresenter.Filter.UserInput,
|
||||
onClick = openFilterSheet,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
// SY -->
|
||||
Text(
|
||||
text = if (presenter.filters.isNotEmpty()) {
|
||||
stringResource(R.string.action_filter)
|
||||
} else {
|
||||
stringResource(R.string.action_search)
|
||||
},
|
||||
)
|
||||
// SY <--
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
AppStateBanners(downloadedOnlyMode, incognitoMode)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
},
|
||||
) { paddingValues ->
|
||||
BrowseSourceContent(
|
||||
state = presenter,
|
||||
mangaList = mangaList,
|
||||
getMangaState = { presenter.getManga(it) },
|
||||
// SY -->
|
||||
getMetadataState = { manga, metadata ->
|
||||
presenter.getRaisedSearchMetadata(manga, metadata)
|
||||
},
|
||||
// SY <--
|
||||
columns = columns,
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
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,
|
||||
) {
|
||||
run {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
text = if (isVisible) {
|
||||
stringResource(R.string.action_filter)
|
||||
} else {
|
||||
stringResource(R.string.saved_searches)
|
||||
},
|
||||
)
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
|
||||
onClick = onFabClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
state: BrowseSourceState,
|
||||
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
// SY -->
|
||||
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
|
||||
// SY <--
|
||||
source: CatalogueSource?,
|
||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
||||
columns: GridCells,
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode: Boolean,
|
||||
// SY <--
|
||||
displayMode: LibraryDisplayMode,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
contentPadding: PaddingValues,
|
||||
@ -287,7 +81,7 @@ fun BrowseSourceContent(
|
||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||
EmptyScreen(
|
||||
message = getErrorMessage(errorState),
|
||||
actions = if (state.source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
|
||||
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.local_source_help_guide,
|
||||
@ -335,11 +129,9 @@ fun BrowseSourceContent(
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (state.source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
|
||||
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
|
||||
BrowseSourceEHentaiList(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
getMetadataState = getMetadataState,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
@ -352,10 +144,6 @@ fun BrowseSourceContent(
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
BrowseSourceComfortableGrid(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
// SY -->
|
||||
getMetadataState = getMetadataState,
|
||||
// SY <--
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
@ -365,22 +153,14 @@ fun BrowseSourceContent(
|
||||
LibraryDisplayMode.List -> {
|
||||
BrowseSourceList(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
// SY -->
|
||||
getMetadataState = getMetadataState,
|
||||
// SY <--
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
BrowseSourceCompactGrid(
|
||||
mangaList = mangaList,
|
||||
getMangaState = getMangaState,
|
||||
// SY -->
|
||||
getMetadataState = getMetadataState,
|
||||
// SY <--
|
||||
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)
|
||||
}
|
@ -26,6 +26,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
|
@ -1,80 +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) },
|
||||
// SY -->
|
||||
getMetadataState = { manga, metadata ->
|
||||
presenter.getRaisedSearchMetadata(manga, metadata)
|
||||
},
|
||||
// SY <--
|
||||
columns = columns,
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
displayMode = presenter.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) },
|
||||
onLocalSourceHelpClick = onHelpClick,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaClick,
|
||||
)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -25,14 +25,11 @@ import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceComfortableGrid(
|
||||
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
// SY -->
|
||||
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
|
||||
// SY <--
|
||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
||||
columns: GridCells,
|
||||
contentPadding: PaddingValues,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
@ -51,10 +48,10 @@ fun BrowseSourceComfortableGrid(
|
||||
}
|
||||
|
||||
items(mangaList.itemCount) { index ->
|
||||
val initialManga = mangaList[index] ?: return@items
|
||||
val manga by getMangaState(initialManga.first)
|
||||
// SY -->
|
||||
val metadata by getMetadataState(initialManga.first, initialManga.second)
|
||||
val pair by mangaList[index]?.collectAsState() ?: return@items
|
||||
val manga = pair.first
|
||||
val metadata = pair.second
|
||||
// SY <--
|
||||
BrowseSourceComfortableGridItem(
|
||||
manga = manga,
|
||||
|
@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -25,14 +25,11 @@ import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceCompactGrid(
|
||||
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
// SY -->
|
||||
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
|
||||
// SY <--
|
||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
||||
columns: GridCells,
|
||||
contentPadding: PaddingValues,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
@ -51,10 +48,10 @@ fun BrowseSourceCompactGrid(
|
||||
}
|
||||
|
||||
items(mangaList.itemCount) { index ->
|
||||
val initialManga = mangaList[index] ?: return@items
|
||||
val manga by getMangaState(initialManga.first)
|
||||
// SY -->
|
||||
val metadata by getMetadataState(initialManga.first, initialManga.second)
|
||||
val pair by mangaList[index]?.collectAsState() ?: return@items
|
||||
val manga = pair.first
|
||||
val metadata = pair.second
|
||||
// SY <--
|
||||
BrowseSourceCompactGridItem(
|
||||
manga = manga,
|
||||
|
@ -1,12 +1,23 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
|
||||
@Composable
|
||||
fun RemoveMangaDialog(
|
||||
@ -39,3 +50,105 @@ fun RemoveMangaDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FailedToLoadSavedSearchDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.save_search_failed_to_load))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.save_search_failed_to_load_message))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavedSearchDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
name: String,
|
||||
deleteSavedSearch: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
deleteSavedSearch()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.save_search_delete))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.save_search_delete_message, name))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavedSearchCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
currentSavedSearches: List<String>,
|
||||
saveSearch: (String) -> Unit,
|
||||
) {
|
||||
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(""))
|
||||
}
|
||||
val context = LocalContext.current
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(R.string.save_search)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(text = stringResource(R.string.save_search_hint))
|
||||
},
|
||||
)
|
||||
},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = true,
|
||||
),
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val searchName = textFieldValue.text.trim()
|
||||
if (searchName.isNotBlank() && searchName !in currentSavedSearches) {
|
||||
saveSearch(searchName)
|
||||
onDismissRequest()
|
||||
} else {
|
||||
context.toast(R.string.save_search_invalid_name)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -46,15 +46,12 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.SourceTagsUtil
|
||||
import exh.util.SourceTagsUtil.GenreColor
|
||||
import exh.util.floor
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceEHentaiList(
|
||||
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
// SY -->
|
||||
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
|
||||
// SY <--
|
||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
||||
contentPadding: PaddingValues,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
@ -69,10 +66,10 @@ fun BrowseSourceEHentaiList(
|
||||
}
|
||||
|
||||
items(mangaList) { initialManga ->
|
||||
initialManga ?: return@items
|
||||
val manga by getMangaState(initialManga.first)
|
||||
val pair by initialManga?.collectAsState() ?: return@items
|
||||
val manga = pair.first
|
||||
// SY -->
|
||||
val metadata by getMetadataState(initialManga.first, initialManga.second)
|
||||
val metadata = pair.second
|
||||
// SY <--
|
||||
BrowseSourceEHentaiListItem(
|
||||
manga = manga,
|
||||
|
@ -0,0 +1,33 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceFloatingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
isVisible: Boolean,
|
||||
onFabClick: () -> Unit,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
text = if (isVisible) {
|
||||
stringResource(R.string.action_filter)
|
||||
} else {
|
||||
stringResource(R.string.saved_searches)
|
||||
},
|
||||
)
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
|
||||
onClick = onFabClick,
|
||||
)
|
||||
}
|
@ -3,7 +3,7 @@ package eu.kanade.presentation.browse.components
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -23,14 +23,11 @@ import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceList(
|
||||
mangaList: LazyPagingItems</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>,
|
||||
getMangaState: @Composable ((Manga) -> State<Manga>),
|
||||
// SY -->
|
||||
getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State<RaisedSearchMetadata?>),
|
||||
// SY <--
|
||||
mangaList: LazyPagingItems<StateFlow</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>,
|
||||
contentPadding: PaddingValues,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
@ -44,12 +41,14 @@ fun BrowseSourceList(
|
||||
}
|
||||
}
|
||||
|
||||
items(mangaList) { initialManga ->
|
||||
initialManga ?: return@items
|
||||
val manga by getMangaState(initialManga.first)
|
||||
items(mangaList) { mangaflow ->
|
||||
mangaflow ?: return@items
|
||||
// SY -->
|
||||
val metadata by getMetadataState(initialManga.first, initialManga.second)
|
||||
val pair by mangaflow.collectAsState()
|
||||
val manga = pair.first
|
||||
val metadata = pair.second
|
||||
// SY <--
|
||||
|
||||
BrowseSourceListItem(
|
||||
manga = manga,
|
||||
// SY -->
|
||||
|
@ -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
|
||||
@ -29,7 +28,8 @@ import exh.source.anyIs
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceToolbar(
|
||||
state: BrowseSourceState,
|
||||
searchQuery: String?,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
source: CatalogueSource?,
|
||||
displayMode: LibraryDisplayMode?,
|
||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||
@ -49,8 +49,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 = {
|
||||
|
@ -59,23 +59,3 @@ fun SourceFeedDeleteDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourceFeedFailedToLoadSavedSearchDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.save_search_failed_to_load))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.save_search_failed_to_load_message))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -3,14 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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.presentation.browse.MigrateSearchScreen
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
|
||||
@ -19,14 +16,8 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId, validSources = validSources) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
// SY -->
|
||||
val migrationScreen = remember {
|
||||
navigator.items.filterIsInstance<MigrationListScreen>().last()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
MigrateSearchScreen(
|
||||
navigateUp = navigator::pop,
|
||||
@ -41,21 +32,16 @@ class MigrateSearchScreen(private val mangaId: Long, private val validSources: L
|
||||
screenModel.lastUsedSourceId.set(it.id)
|
||||
}
|
||||
// SY -->
|
||||
router.pushController(
|
||||
SourceSearchController(state.manga!!, it, state.searchQuery)
|
||||
.also { searchController ->
|
||||
searchController.useMangaForMigration = { newMangaId ->
|
||||
migrationScreen.newSelectedItem = mangaId to newMangaId
|
||||
navigator.pop()
|
||||
}
|
||||
},
|
||||
)
|
||||
navigator.push(SourceSearchScreen(state.manga!!, it.id, state.searchQuery))
|
||||
// SY <--
|
||||
},
|
||||
onClickItem = {
|
||||
// SY -->
|
||||
migrationScreen.newSelectedItem = mangaId to it.id
|
||||
navigator.pop()
|
||||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
.last()
|
||||
.newSelectedItem = mangaId to it.id
|
||||
navigator.popUntil { it is MigrationListScreen }
|
||||
// SY <--
|
||||
},
|
||||
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||
|
@ -2,18 +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.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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.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, source: CatalogueSource, searchQuery: String? = null) : this(
|
||||
bundleOf(
|
||||
@ -23,33 +19,16 @@ class SourceSearchController(
|
||||
),
|
||||
)
|
||||
|
||||
var useMangaForMigration: ((Long) -> Unit)? = null
|
||||
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() },
|
||||
// SY -->
|
||||
onMangaClick = { manga ->
|
||||
useMangaForMigration?.let { it(manga.id) }
|
||||
router.popCurrentController()
|
||||
},
|
||||
// SY <--
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
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,114 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
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.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.browse.components.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
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 = {
|
||||
// SY -->
|
||||
BrowseSourceFloatingActionButton(
|
||||
isVisible = state.filters.isNotEmpty(),
|
||||
onFabClick = screenModel::openFilterSheet,
|
||||
)
|
||||
// SY <--
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
val mangaList = remember(state.currentFilter) {
|
||||
screenModel.getMangaListFlow(state.currentFilter)
|
||||
}.collectAsLazyPagingItems()
|
||||
val openMigrateDialog: (Manga) -> Unit = {
|
||||
// SY -->
|
||||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
.last()
|
||||
.newSelectedItem = oldManga.id to it.id
|
||||
navigator.popUntil { it is MigrationListScreen }
|
||||
// SY <--
|
||||
}
|
||||
BrowseSourceContent(
|
||||
source = screenModel.source,
|
||||
mangaList = mangaList,
|
||||
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.filters) {
|
||||
screenModel.initFilterSheet(context, router)
|
||||
}
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ fun Screen.sourcesTab(
|
||||
val controller = when {
|
||||
smartSearchConfig != null -> SmartSearchController(source.id, smartSearchConfig)
|
||||
(query.isBlank() || query == QUERY_POPULAR) && screenModel.useNewSourceNavigation -> SourceFeedController(source.id)
|
||||
else -> BrowseSourceController(source, query)
|
||||
else -> BrowseSourceController(source.id, query)
|
||||
}
|
||||
screenModel.onOpenSource(source)
|
||||
router.pushController(controller)
|
||||
|
@ -1,39 +1,20 @@
|
||||
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.R
|
||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
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.extension.details.SourcePreferencesController
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
||||
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 eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
import exh.savedsearches.EXHSavedSearch
|
||||
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,
|
||||
@ -98,200 +79,41 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
filterList,
|
||||
)
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
protected var filterSheet: SourceFilterSheet? = null
|
||||
private val sourceId = args.getLong(SOURCE_ID_KEY)
|
||||
private val initialQuery = args.getString(SEARCH_QUERY_KEY)
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
// SY -->
|
||||
return BrowseSourcePresenter(
|
||||
args.getLong(SOURCE_ID_KEY),
|
||||
args.getString(SEARCH_QUERY_KEY),
|
||||
filtersJson = args.getString(FILTERS_CONFIG_KEY),
|
||||
savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L },
|
||||
)
|
||||
// SY <--
|
||||
}
|
||||
// SY -->
|
||||
private val filtersJson = args.getString(FILTERS_CONFIG_KEY)
|
||||
private val savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L }
|
||||
// SY <--
|
||||
|
||||
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,
|
||||
// SY -->
|
||||
filtersJson = filtersJson,
|
||||
savedSearch = savedSearch,
|
||||
// SY <--
|
||||
),
|
||||
) { 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)
|
||||
},
|
||||
// SY -->
|
||||
onSettingsClick = {
|
||||
router.pushController(SourcePreferencesController(presenter.source!!.id))
|
||||
},
|
||||
// SY <--
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
fun setSavedSearches(savedSearches: List<EXHSavedSearch>) {
|
||||
filterSheet?.setSavedSearches(savedSearches)
|
||||
}
|
||||
|
||||
open fun initFilterSheet() {
|
||||
filterSheet = SourceFilterSheet(
|
||||
activity!!,
|
||||
// SY -->
|
||||
router,
|
||||
presenter.source!!,
|
||||
emptyList(),
|
||||
// SY <--
|
||||
onFilterClicked = {
|
||||
presenter.search(filters = presenter.filters)
|
||||
},
|
||||
onResetClicked = {
|
||||
filterSheet?.dismiss()
|
||||
presenter.reset()
|
||||
},
|
||||
// EXH -->
|
||||
onSaveClicked = {
|
||||
viewScope.launchUI {
|
||||
filterSheet?.context?.let {
|
||||
val names = presenter.loadSearches().map { it.name }
|
||||
var searchName = ""
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.save_search)
|
||||
.setTextInput(hint = it.getString(R.string.save_search_hint)) { input ->
|
||||
searchName = input
|
||||
}
|
||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||
if (searchName.isNotBlank() && searchName !in names) {
|
||||
presenter.saveSearch(
|
||||
name = searchName.trim(),
|
||||
query = presenter.currentFilter.query,
|
||||
filterList = presenter.currentFilter.filters.ifEmpty { presenter.source!!.getFilterList() },
|
||||
)
|
||||
} else {
|
||||
it.toast(R.string.save_search_invalid_name)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSavedSearchClicked = { idOfSearch ->
|
||||
viewScope.launchUI {
|
||||
val search = presenter.loadSearch(idOfSearch)
|
||||
|
||||
if (search == null) {
|
||||
filterSheet?.context?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.save_search_failed_to_load)
|
||||
.setMessage(R.string.save_search_failed_to_load_message)
|
||||
.show()
|
||||
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)
|
||||
}
|
||||
return@launchUI
|
||||
}
|
||||
|
||||
if (search.filterList == null && presenter.filters.isNotEmpty()) {
|
||||
activity?.toast(R.string.save_search_invalid)
|
||||
return@launchUI
|
||||
}
|
||||
|
||||
val allDefault = search.filterList != null && presenter.filters == presenter.source!!.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
|
||||
presenter.search(
|
||||
query = search.query,
|
||||
filters = if (allDefault) null else search.filterList,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSavedSearchDeleteClicked = { idToDelete, name ->
|
||||
filterSheet?.context?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.save_search_delete)
|
||||
.setMessage(it.getString(R.string.save_search_delete_message, name))
|
||||
.setPositiveButton(R.string.action_cancel, null)
|
||||
.setNegativeButton(android.R.string.ok) { _, _ ->
|
||||
presenter.deleteSearch(idToDelete)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
},
|
||||
// EXH <--
|
||||
)
|
||||
launchUI {
|
||||
filterSheet?.setSavedSearches(presenter.loadSearches())
|
||||
}
|
||||
}
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,7 +122,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)) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,52 +133,15 @@ 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"
|
||||
|
||||
// SY -->
|
||||
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||
const val SAVED_SEARCH_CONFIG_KEY = "savedSearch"
|
||||
const val FILTERS_CONFIG_KEY = "filters"
|
||||
// SY <--
|
||||
viewScope.launch { queryEvent.send(BrowseSourceScreen.SearchType.Genre(genreName)) }
|
||||
}
|
||||
}
|
||||
|
||||
private const val SOURCE_ID_KEY = "sourceId"
|
||||
private const val SEARCH_QUERY_KEY = "searchQuery"
|
||||
|
||||
// SY -->
|
||||
private const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||
private const val SAVED_SEARCH_CONFIG_KEY = "savedSearch"
|
||||
private const val FILTERS_CONFIG_KEY = "filters"
|
||||
// SY <--
|
||||
|
@ -0,0 +1,326 @@
|
||||
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.FailedToLoadSavedSearchDialog
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.browse.components.SavedSearchCreateDialog
|
||||
import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog
|
||||
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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
|
||||
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,
|
||||
// SY -->
|
||||
private val filtersJson: String? = null,
|
||||
private val savedSearch: Long? = null,
|
||||
// SY <--
|
||||
) : 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,
|
||||
// SY -->
|
||||
filtersJson = filtersJson,
|
||||
savedSearch = savedSearch,
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
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) },
|
||||
// SY -->
|
||||
onSettingsClick = { router.pushController(SourcePreferencesController(sourceId)) },
|
||||
// SY <--
|
||||
)
|
||||
|
||||
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))
|
||||
},
|
||||
)
|
||||
}
|
||||
/* SY --> if (state.filters.isNotEmpty())*/ run /* SY <-- */ {
|
||||
FilterChip(
|
||||
selected = state.currentFilter is BrowseSourceScreenModel.Filter.UserInput,
|
||||
onClick = screenModel::openFilterSheet,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
// SY -->
|
||||
Text(
|
||||
text = if (state.filters.isNotEmpty()) {
|
||||
stringResource(R.string.action_filter)
|
||||
} else {
|
||||
stringResource(R.string.action_search)
|
||||
},
|
||||
)
|
||||
// SY <--
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourceScreenModel.Dialog.CreateSavedSearh -> SavedSearchCreateDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
currentSavedSearches = dialog.currentSavedSearches,
|
||||
saveSearch = screenModel::saveSearch,
|
||||
)
|
||||
is BrowseSourceScreenModel.Dialog.DeleteSavedSearch -> SavedSearchDeleteDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
name = dialog.name,
|
||||
deleteSavedSearch = {
|
||||
screenModel.deleteSearch(dialog.idToDelete)
|
||||
},
|
||||
)
|
||||
BrowseSourceScreenModel.Dialog.FailedToLoadSavedSearch -> FailedToLoadSavedSearchDialog(onDismissRequest)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
BackHandler(onBack = navigateUp)
|
||||
|
||||
LaunchedEffect(state.filters) {
|
||||
screenModel.initFilterSheet(context, router)
|
||||
}
|
||||
|
||||
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,23 @@
|
||||
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 com.bluelinelabs.conductor.Router
|
||||
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.UnsortedPreferences
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
@ -45,8 +45,7 @@ import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
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.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@ -54,10 +53,8 @@ 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.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
|
||||
@ -80,16 +77,21 @@ import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.savedsearches.models.SavedSearch
|
||||
import exh.source.getMainSource
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
@ -101,15 +103,15 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
|
||||
import java.util.Date
|
||||
import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
|
||||
|
||||
open class BrowseSourcePresenter(
|
||||
open class BrowseSourceScreenModel(
|
||||
private val sourceId: Long,
|
||||
searchQuery: String? = null,
|
||||
searchQuery: String?,
|
||||
// SY -->
|
||||
private val filtersJson: String? = null,
|
||||
private val savedSearch: Long? = null,
|
||||
// SY <--
|
||||
private val state: BrowseSourceStateImpl = BrowseSourceState(searchQuery) as BrowseSourceStateImpl,
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
@ -134,119 +136,35 @@ open class BrowseSourcePresenter(
|
||||
private val insertSavedSearch: InsertSavedSearch = Injekt.get(),
|
||||
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
|
||||
// SY <--
|
||||
) : 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)
|
||||
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
private var filterSheet: SourceFilterSheet? = null
|
||||
|
||||
// SY -->
|
||||
val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState()
|
||||
// SY <--
|
||||
val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMangaList(): Flow<PagingData</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>> {
|
||||
return remember(currentFilter) {
|
||||
Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
createSourcePagingSource(currentFilter.query, currentFilter.filters)
|
||||
}.flow
|
||||
.map {
|
||||
it.map { (sManga, metadata) ->
|
||||
// SY -->
|
||||
withIOContext {
|
||||
networkToLocalManga.await(sManga.toDomainManga(sourceId))
|
||||
} to metadata
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@Composable
|
||||
open fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
|
||||
return produceState(initialValue = initialMetadata, manga.id) {
|
||||
val source = source?.getMainSource<MetadataSource<*, *>>() ?: return@produceState
|
||||
getFlatMetadataById.subscribe(manga.id)
|
||||
.collectLatest { metadata ->
|
||||
if (metadata == null) return@collectLatest
|
||||
value = metadata.raise(source.metaClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun reset() {
|
||||
val source = source ?: return
|
||||
state.filters = source.getFilterList()
|
||||
}
|
||||
|
||||
fun search(query: String? = null, filters: FilterList? = null) {
|
||||
// SY -->
|
||||
if (filters != null && filters !== state.filters) {
|
||||
state.filters = filters
|
||||
}
|
||||
// SY <--
|
||||
Filter.valueOf(query ?: "").let {
|
||||
if (it !is Filter.UserInput) {
|
||||
state.currentFilter = it
|
||||
state.searchQuery = null
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private val filterSerializer = FilterSerializer()
|
||||
// SY <--
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
||||
state.filters = source!!.getFilterList()
|
||||
init {
|
||||
mutableState.update { it.copy(filters = source.getFilterList()) }
|
||||
|
||||
// SY -->
|
||||
val savedSearchFilters = savedSearch
|
||||
val jsonFilters = filtersJson
|
||||
val filters = state.value.filters
|
||||
if (savedSearchFilters != null) {
|
||||
val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { filters } }
|
||||
if (savedSearch != null) {
|
||||
@ -254,22 +172,140 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
} else if (jsonFilters != null) {
|
||||
runCatching {
|
||||
val filters = Json.decodeFromString<JsonArray>(jsonFilters)
|
||||
filterSerializer.deserialize(this.filters, filters)
|
||||
search(filters = this.filters)
|
||||
val filtersJson = Json.decodeFromString<JsonArray>(jsonFilters)
|
||||
filterSerializer.deserialize(filters, filtersJson)
|
||||
search(filters = filters)
|
||||
}
|
||||
}
|
||||
|
||||
getExhSavedSearch.subscribe(source!!.id, source!!::getFilterList)
|
||||
getExhSavedSearch.subscribe(source.id, source::getFilterList)
|
||||
.onEach {
|
||||
withUIContext {
|
||||
view?.setSavedSearches(it)
|
||||
filterSheet?.setSavedSearches(it)
|
||||
}
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
.launchIn(coroutineScope)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
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</* SY --> */Pair<Manga, RaisedSearchMetadata?>/* SY <-- */>>> {
|
||||
return Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
// SY -->
|
||||
createSourcePagingSource(currentFilter.query ?: "", currentFilter.filters)
|
||||
// SY <--
|
||||
}.flow
|
||||
.map { pagingData ->
|
||||
pagingData.map { (sManga, metadata) ->
|
||||
val dbManga = withIOContext { networkToLocalManga.await(sManga.toDomainManga(sourceId)) }
|
||||
getManga.subscribe(dbManga.url, dbManga.source)
|
||||
.filterNotNull()
|
||||
.onEach { initializeManga(it) }
|
||||
// SY -->
|
||||
.combineMetadata(dbManga, metadata)
|
||||
// SY <--
|
||||
.stateIn(coroutineScope)
|
||||
}
|
||||
}
|
||||
.cachedIn(coroutineScope)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
open fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
|
||||
val metadataSource = source.getMainSource<MetadataSource<*, *>>()
|
||||
return combine(getFlatMetadataById.subscribe(dbManga.id)) { manga, flatMetadata ->
|
||||
metadataSource ?: return@combine manga to metadata
|
||||
manga to (flatMetadata?.raise(metadataSource.metaClass) ?: metadata)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun reset() {
|
||||
mutableState.update { it.copy(filters = source.getFilterList()) }
|
||||
}
|
||||
|
||||
fun search(query: String? = null, filters: FilterList? = null) {
|
||||
// SY -->
|
||||
if (filters != null && filters !== state.value.filters) {
|
||||
mutableState.update { state -> state.copy(filters = filters) }
|
||||
}
|
||||
// SY <--
|
||||
Filter.valueOf(query).let {
|
||||
if (it !is Filter.UserInput) {
|
||||
mutableState.update { state -> state.copy(currentFilter = it) }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun searchGenre(genreName: String) {
|
||||
val defaultFilters = source.getFilterList()
|
||||
var genreExists = false
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a manga.
|
||||
*
|
||||
@ -279,7 +315,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)
|
||||
|
||||
@ -296,7 +332,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) {
|
||||
@ -322,7 +358,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() }
|
||||
@ -345,7 +381,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 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -354,7 +390,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 ->
|
||||
@ -373,7 +409,7 @@ open class BrowseSourcePresenter(
|
||||
|
||||
// SY -->
|
||||
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return getRemoteManga.subscribe(sourceId, currentFilter.query, currentFilter.filters)
|
||||
return getRemoteManga.subscribe(sourceId, query, filters)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@ -398,7 +434,7 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
|
||||
fun moveMangaToCategories(manga: Manga, categoryIds: List<Long>) {
|
||||
presenterScope.launchIO {
|
||||
coroutineScope.launchIO {
|
||||
setMangaCategories.await(
|
||||
mangaId = manga.id,
|
||||
categoryIds = categoryIds.toList(),
|
||||
@ -406,13 +442,82 @@ 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) }
|
||||
}
|
||||
|
||||
open fun initFilterSheet(context: Context, router: Router) {
|
||||
val state = state.value
|
||||
/*if (state.filters.isEmpty()) {
|
||||
return
|
||||
}*/
|
||||
|
||||
filterSheet = SourceFilterSheet(
|
||||
context = context,
|
||||
// SY -->
|
||||
router = router,
|
||||
source = source,
|
||||
searches = emptyList(),
|
||||
// SY <--
|
||||
onFilterClicked = { search(filters = state.filters) },
|
||||
onResetClicked = {
|
||||
reset()
|
||||
filterSheet?.setFilters(state.filterItems)
|
||||
},
|
||||
// EXH -->
|
||||
onSaveClicked = {
|
||||
coroutineScope.launchIO {
|
||||
val names = loadSearches().map { it.name }
|
||||
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearh(names)) }
|
||||
}
|
||||
},
|
||||
onSavedSearchClicked = { idOfSearch ->
|
||||
coroutineScope.launchIO {
|
||||
val search = loadSearch(idOfSearch)
|
||||
|
||||
if (search == null) {
|
||||
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
if (search.filterList == null && state.filters.isNotEmpty()) {
|
||||
context.toast(R.string.save_search_invalid)
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val allDefault = search.filterList != null && state.filters == source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
|
||||
search(
|
||||
query = search.query,
|
||||
filters = if (allDefault) null else search.filterList,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSavedSearchDeleteClicked = { idToDelete, name ->
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(idToDelete, name)) }
|
||||
},
|
||||
// EXH <--
|
||||
)
|
||||
|
||||
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
|
||||
@ -430,17 +535,42 @@ open class BrowseSourcePresenter(
|
||||
val initialSelection: List<CheckboxState.State<Category>>,
|
||||
) : Dialog()
|
||||
data class Migrate(val newManga: Manga) : Dialog()
|
||||
|
||||
// SY -->
|
||||
object FailedToLoadSavedSearch : Dialog()
|
||||
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog()
|
||||
data class CreateSavedSearh(val currentSavedSearches: List<String>) : Dialog()
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun saveSearch(name: String, query: String, filterList: FilterList) {
|
||||
presenterScope.launchNonCancellable {
|
||||
fun saveSearch(
|
||||
name: String,
|
||||
) {
|
||||
coroutineScope.launchNonCancellable {
|
||||
val query = state.value.currentFilter.query
|
||||
val filterList = state.value.currentFilter.filters.ifEmpty { source.getFilterList() }
|
||||
insertSavedSearch.await(
|
||||
SavedSearch(
|
||||
id = -1,
|
||||
source = source!!.id,
|
||||
source = source.id,
|
||||
name = name.trim(),
|
||||
query = query.nullIfBlank(),
|
||||
query = query?.nullIfBlank(),
|
||||
filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(),
|
||||
),
|
||||
)
|
||||
@ -448,41 +578,41 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
|
||||
fun deleteSearch(savedSearchId: Long) {
|
||||
presenterScope.launchNonCancellable {
|
||||
coroutineScope.launchNonCancellable {
|
||||
deleteSavedSearchById.await(savedSearchId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSearch(searchId: Long) =
|
||||
getExhSavedSearch.awaitOne(searchId, source!!::getFilterList)
|
||||
getExhSavedSearch.awaitOne(searchId, source::getFilterList)
|
||||
|
||||
suspend fun loadSearches() =
|
||||
getExhSavedSearch.await(source!!.id, source!!::getFilterList)
|
||||
getExhSavedSearch.await(source.id, source::getFilterList)
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull { filter ->
|
||||
when (filter) {
|
||||
is Filter.Header -> HeaderItem(filter)
|
||||
// --> EXH
|
||||
is Filter.AutoComplete -> AutoComplete(filter)
|
||||
is SourceModelFilter.AutoComplete -> AutoComplete(filter)
|
||||
// <-- EXH
|
||||
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)
|
||||
// SY -->
|
||||
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
is SourceModelFilter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
// SY <--
|
||||
else -> null
|
||||
}
|
||||
@ -491,7 +621,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)
|
@ -17,9 +17,9 @@ import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||
import eu.kanade.presentation.browse.SourceFeedScreen
|
||||
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
|
||||
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
|
||||
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
|
||||
import eu.kanade.presentation.browse.components.SourceFeedFailedToLoadSavedSearchDialog
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
@ -89,7 +89,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen {
|
||||
)
|
||||
}
|
||||
SourceFeedScreenModel.Dialog.FailedToLoadSavedSearch -> {
|
||||
SourceFeedFailedToLoadSavedSearchDialog(onDismissRequest)
|
||||
FailedToLoadSavedSearchDialog(onDismissRequest)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
|
@ -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)) },
|
||||
|
@ -2,24 +2,16 @@ package exh.md.follows
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.core.os.bundleOf
|
||||
import eu.kanade.presentation.browse.BrowseMangadexFollowsScreen
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.components.DuplicateMangaDialog
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
*/
|
||||
class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
class MangaDexFollowsController(bundle: Bundle) : BasicFullComposeController(bundle) {
|
||||
|
||||
constructor(source: CatalogueSource) : this(
|
||||
bundleOf(
|
||||
@ -27,71 +19,12 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
|
||||
),
|
||||
)
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
private val sourceId = args.getLong(SOURCE_ID_KEY)
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BrowseMangadexFollowsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
onDisplayModeChange = { presenter.displayMode = (it) },
|
||||
onMangaClick = {
|
||||
router.pushController(MangaController(it.id, true))
|
||||
},
|
||||
onMangaLongClick = { manga ->
|
||||
scope.launchIO {
|
||||
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
manga.favorite -> presenter.dialog = BrowseSourcePresenter.Dialog.RemoveManga(manga)
|
||||
duplicateManga != null -> presenter.dialog = BrowseSourcePresenter.Dialog.AddDuplicateManga(manga, duplicateManga)
|
||||
else -> presenter.addFavorite(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val onDismissRequest = { presenter.dialog = null }
|
||||
when (val dialog = presenter.dialog) {
|
||||
is BrowseSourcePresenter.Dialog.AddDuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { presenter.addFavorite(dialog.manga) },
|
||||
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
|
||||
duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
|
||||
)
|
||||
}
|
||||
is BrowseSourcePresenter.Dialog.RemoveManga -> {
|
||||
RemoveMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = {
|
||||
presenter.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
mangaToRemove = dialog.manga,
|
||||
)
|
||||
}
|
||||
is BrowseSourcePresenter.Dialog.ChangeMangaCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = {
|
||||
router.pushController(CategoryController())
|
||||
},
|
||||
onConfirm = { include, _ ->
|
||||
presenter.changeMangaFavorite(dialog.manga)
|
||||
presenter.moveMangaToCategories(dialog.manga, include)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourcePresenter.Dialog.Migrate -> Unit
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in mangadex follows
|
||||
Navigator(screen = MangaDexFollowsScreen(sourceId))
|
||||
}
|
||||
}
|
||||
|
||||
private const val SOURCE_ID_KEY = "source_id"
|
||||
|
@ -1,28 +0,0 @@
|
||||
package exh.md.follows
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
|
||||
/**
|
||||
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
||||
|
||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return MangaDexFollowsPagingSource(source!!.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
|
||||
return remember { mutableStateOf(initialMetadata) }
|
||||
}
|
||||
}
|
143
app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt
Normal file
143
app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt
Normal file
@ -0,0 +1,143 @@
|
||||
package exh.md.follows
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
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.presentation.browse.BrowseSourceContent
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||
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.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
|
||||
class MangaDexFollowsScreen(private val sourceId: Long) : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val screenModel = rememberScreenModel { MangaDexFollowsScreenModel(sourceId) }
|
||||
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 ->
|
||||
BrowseSourceSimpleToolbar(
|
||||
title = stringResource(R.string.mangadex_follows),
|
||||
displayMode = screenModel.displayMode,
|
||||
onDisplayModeChange = { screenModel.displayMode = it },
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
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),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = screenModel.ehentaiBrowseDisplayMode,
|
||||
// SY <--
|
||||
displayMode = screenModel.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = null,
|
||||
onHelpClick = null,
|
||||
onLocalSourceHelpClick = null,
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package exh.md.follows
|
||||
|
||||
import android.content.Context
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
|
||||
|
||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
override fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
|
||||
return map { it to metadata }
|
||||
}
|
||||
|
||||
override fun initFilterSheet(context: Context, router: Router) {
|
||||
// No-op: we don't allow filtering in recs
|
||||
}
|
||||
}
|
@ -2,54 +2,33 @@ package exh.md.similar
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.BrowseRecommendationsScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
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
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
*/
|
||||
class MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
class MangaDexSimilarController(bundle: Bundle) : BasicFullComposeController(bundle) {
|
||||
|
||||
constructor(manga: Manga, source: CatalogueSource) : this(
|
||||
bundleOf(
|
||||
MANGA_ID to manga.id,
|
||||
MANGA_TITLE to manga.title,
|
||||
SOURCE_ID_KEY to source.id,
|
||||
),
|
||||
)
|
||||
|
||||
private val mangaTitle = args.getString(MANGA_TITLE, "")
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return MangaDexSimilarPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
val mangaId = args.getLong(MANGA_ID)
|
||||
val sourceId = args.getLong(SOURCE_ID_KEY)
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BrowseRecommendationsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
title = stringResource(R.string.similar, mangaTitle),
|
||||
onMangaClick = {
|
||||
router.pushController(MangaController(it.id, true))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in similar
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MANGA_ID = "manga_id"
|
||||
const val MANGA_TITLE = "manga_title"
|
||||
Navigator(screen = MangaDexSimilarScreen(mangaId, sourceId))
|
||||
}
|
||||
}
|
||||
|
||||
private const val MANGA_ID = "manga_id"
|
||||
private const val SOURCE_ID_KEY = "source_id"
|
||||
|
@ -1,44 +0,0 @@
|
||||
package exh.md.similar
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class MangaDexSimilarPresenter(
|
||||
val mangaId: Long,
|
||||
sourceId: Long,
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) : BrowseSourcePresenter(sourceId) {
|
||||
|
||||
var manga: Manga? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
this.manga = runBlocking { getManga.await(mangaId) }
|
||||
}
|
||||
|
||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return MangaDexSimilarPagingSource(manga!!, source!!.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
|
||||
return remember { mutableStateOf(initialMetadata) }
|
||||
}
|
||||
}
|
81
app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt
Normal file
81
app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt
Normal file
@ -0,0 +1,81 @@
|
||||
package exh.md.similar
|
||||
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.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.browse.components.BrowseSourceSimpleToolbar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
|
||||
class MangaDexSimilarScreen(val mangaId: Long, val sourceId: Long) : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val screenModel = rememberScreenModel { MangaDexSimilarScreenModel(mangaId, sourceId) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val onMangaClick: (Manga) -> Unit = {
|
||||
router.pushController(MangaController(it.id, true))
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val navigateUp: () -> Unit = {
|
||||
when {
|
||||
navigator.canPop -> navigator.pop()
|
||||
router.backstackSize > 1 -> router.popCurrentController()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
BrowseSourceSimpleToolbar(
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(R.string.similar, screenModel.manga.title),
|
||||
displayMode = screenModel.displayMode,
|
||||
onDisplayModeChange = { screenModel.displayMode = it },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
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),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = false,
|
||||
// SY <--
|
||||
displayMode = screenModel.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = null,
|
||||
onHelpClick = null,
|
||||
onLocalSourceHelpClick = null,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package exh.md.similar
|
||||
|
||||
import android.content.Context
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class MangaDexSimilarScreenModel(
|
||||
val mangaId: Long,
|
||||
sourceId: Long,
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) : BrowseSourceScreenModel(sourceId, null) {
|
||||
|
||||
val manga: Manga = runBlocking { getManga.await(mangaId) }!!
|
||||
|
||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return MangaDexSimilarPagingSource(manga, source.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
override fun Flow<Manga>.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow<Pair<Manga, RaisedSearchMetadata?>> {
|
||||
return map { it to metadata }
|
||||
}
|
||||
|
||||
override fun initFilterSheet(context: Context, router: Router) {
|
||||
// No-op: we don't allow filtering in recs
|
||||
}
|
||||
}
|
@ -3,17 +3,16 @@ package exh.recs
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.os.bundleOf
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.BrowseRecommendationsScreen
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
*/
|
||||
class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
class RecommendsController(bundle: Bundle) : BasicFullComposeController(bundle) {
|
||||
|
||||
constructor(manga: Manga, source: CatalogueSource) : this(
|
||||
bundleOf(
|
||||
@ -22,38 +21,14 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
),
|
||||
)
|
||||
|
||||
override fun createPresenter(): RecommendsPresenter {
|
||||
return RecommendsPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
val mangaId = args.getLong(MANGA_ID)
|
||||
val sourceId = args.getLong(SOURCE_ID_KEY)
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BrowseRecommendationsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
title = (presenter as RecommendsPresenter).manga!!.title,
|
||||
onMangaClick = { manga ->
|
||||
openSmartSearch(manga.ogTitle)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in recs
|
||||
}
|
||||
|
||||
private fun openSmartSearch(title: String) {
|
||||
val smartSearchConfig = SourcesController.SmartSearchConfig(title)
|
||||
router.pushController(
|
||||
SourcesController(
|
||||
bundleOf(
|
||||
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MANGA_ID = "manga_id"
|
||||
Navigator(screen = RecommendsScreen(mangaId, sourceId))
|
||||
}
|
||||
}
|
||||
|
||||
private const val MANGA_ID = "manga_id"
|
||||
private const val SOURCE_ID_KEY = "source_id"
|
||||
|
92
app/src/main/java/exh/recs/RecommendsScreen.kt
Normal file
92
app/src/main/java/exh/recs/RecommendsScreen.kt
Normal file
@ -0,0 +1,92 @@
|
||||
package exh.recs
|
||||
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.core.os.bundleOf
|
||||
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 com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceSimpleToolbar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
||||
|
||||
class RecommendsScreen(val mangaId: Long, val sourceId: Long) : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val screenModel = rememberScreenModel { RecommendsScreenModel(mangaId, sourceId) }
|
||||
val state by screenModel.state.collectAsState()
|
||||
val router = LocalRouter.currentOrThrow
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val onMangaClick: (Manga) -> Unit = { manga ->
|
||||
openSmartSearch(router, manga.ogTitle)
|
||||
}
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val navigateUp: () -> Unit = {
|
||||
when {
|
||||
navigator.canPop -> navigator.pop()
|
||||
router.backstackSize > 1 -> router.popCurrentController()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
BrowseSourceSimpleToolbar(
|
||||
navigateUp = navigateUp,
|
||||
title = screenModel.manga.title,
|
||||
displayMode = screenModel.displayMode,
|
||||
onDisplayModeChange = { screenModel.displayMode = it },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
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),
|
||||
// SY -->
|
||||
ehentaiBrowseDisplayMode = false,
|
||||
// SY <--
|
||||
displayMode = screenModel.displayMode,
|
||||
snackbarHostState = snackbarHostState,
|
||||
contentPadding = paddingValues,
|
||||
onWebViewClick = null,
|
||||
onHelpClick = null,
|
||||
onLocalSourceHelpClick = null,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSmartSearch(router: Router, title: String) {
|
||||
val smartSearchConfig = SourcesController.SmartSearchConfig(title)
|
||||
router.pushController(
|
||||
SourcesController(
|
||||
bundleOf(
|
||||
SourcesController.SMART_SEARCH_CONFIG to smartSearchConfig,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,11 +1,9 @@
|
||||
package exh.recs
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -13,20 +11,15 @@ import uy.kohesive.injekt.api.get
|
||||
/**
|
||||
* Presenter of [RecommendsController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class RecommendsPresenter(
|
||||
class RecommendsScreenModel(
|
||||
val mangaId: Long,
|
||||
sourceId: Long,
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) : BrowseSourcePresenter(sourceId) {
|
||||
) : BrowseSourceScreenModel(sourceId, null) {
|
||||
|
||||
var manga: Manga? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
this.manga = runBlocking { getManga.await(mangaId) }
|
||||
}
|
||||
val manga = runBlocking { getManga.await(mangaId) }!!
|
||||
|
||||
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType {
|
||||
return RecommendsPagingSource(source!!, manga!!)
|
||||
return RecommendsPagingSource(source, manga)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user