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:
Ivan Iskandar 2022-12-01 11:05:11 +07:00 committed by Jobobby04
parent 6185c95715
commit 6402258c83
37 changed files with 1396 additions and 1231 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import eu.kanade.domain.library.model.LibraryDisplayMode
import eu.kanade.presentation.browse.BrowseSourceState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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