From 6402258c8380bac662b5c2bd16f4a57a741caed9 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Thu, 1 Dec 2022 11:05:11 +0700 Subject: [PATCH] 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 --- .../data/manga/MangaMetadataRepositoryImpl.kt | 6 +- .../manga/interactor/GetFlatMetadataById.kt | 2 +- .../repository/MangaMetadataRepository.kt | 6 +- .../browse/BrowseMangadexFollowsScreen.kt | 68 --- .../browse/BrowseRecommendationsScreen.kt | 56 --- .../presentation/browse/BrowseSourceScreen.kt | 238 +--------- .../presentation/browse/BrowseSourceState.kt | 41 -- .../presentation/browse/SourceFeedScreen.kt | 1 + .../presentation/browse/SourceSearchScreen.kt | 80 ---- .../components/BrowseSourceComfortableGrid.kt | 15 +- .../components/BrowseSourceCompactGrid.kt | 15 +- .../browse/components/BrowseSourceDialogs.kt | 113 +++++ .../components/BrowseSourceEHentaiList.kt | 15 +- .../BrowseSourceFloatingActionButton.kt | 33 ++ .../browse/components/BrowseSourceList.kt | 19 +- .../browse/components/BrowseSourceToolbar.kt | 8 +- .../browse/components/SourceFeedDialogs.kt | 20 - .../migration/search/MigrateSearchScreen.kt | 26 +- .../search/SourceSearchController.kt | 41 +- .../migration/search/SourceSearchScreen.kt | 114 +++++ .../tachiyomi/ui/browse/source/SourcesTab.kt | 2 +- .../source/browse/BrowseSourceController.kt | 309 ++----------- .../source/browse/BrowseSourceScreen.kt | 326 +++++++++++++ ...resenter.kt => BrowseSourceScreenModel.kt} | 430 ++++++++++++------ .../ui/browse/source/feed/SourceFeedScreen.kt | 4 +- .../source/globalsearch/GlobalSearchScreen.kt | 2 +- .../md/follows/MangaDexFollowsController.kt | 81 +--- .../md/follows/MangaDexFollowsPresenter.kt | 28 -- .../exh/md/follows/MangaDexFollowsScreen.kt | 143 ++++++ .../md/follows/MangaDexFollowsScreenModel.kt | 28 ++ .../md/similar/MangaDexSimilarController.kt | 39 +- .../md/similar/MangaDexSimilarPresenter.kt | 44 -- .../exh/md/similar/MangaDexSimilarScreen.kt | 81 ++++ .../md/similar/MangaDexSimilarScreenModel.kt | 41 ++ .../java/exh/recs/RecommendsController.kt | 43 +- .../main/java/exh/recs/RecommendsScreen.kt | 92 ++++ ...sPresenter.kt => RecommendsScreenModel.kt} | 17 +- 37 files changed, 1396 insertions(+), 1231 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt delete mode 100644 app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/{BrowseSourcePresenter.kt => BrowseSourceScreenModel.kt} (56%) delete mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt delete mode 100644 app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt create mode 100644 app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt create mode 100644 app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt create mode 100644 app/src/main/java/exh/recs/RecommendsScreen.kt rename app/src/main/java/exh/recs/{RecommendsPresenter.kt => RecommendsScreenModel.kt} (56%) diff --git a/app/src/main/java/eu/kanade/data/manga/MangaMetadataRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaMetadataRepositoryImpl.kt index 293b56369..b26596275 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaMetadataRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaMetadataRepositoryImpl.kt @@ -22,7 +22,7 @@ class MangaMetadataRepositoryImpl( return handler.awaitOneOrNull { search_metadataQueries.selectByMangaId(id, searchMetadataMapper) } } - override suspend fun subscribeMetadataById(id: Long): Flow { + override fun subscribeMetadataById(id: Long): Flow { 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> { + override fun subscribeTagsById(id: Long): Flow> { 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> { + override fun subscribeTitlesById(id: Long): Flow> { return handler.subscribeToList { search_titlesQueries.selectByMangaId(id, searchTitleMapper) } } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetFlatMetadataById.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetFlatMetadataById.kt index 905b42911..c67a557ba 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/GetFlatMetadataById.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetFlatMetadataById.kt @@ -29,7 +29,7 @@ class GetFlatMetadataById( } } - suspend fun subscribe(id: Long): Flow { + fun subscribe(id: Long): Flow { return combine( mangaMetadataRepository.subscribeMetadataById(id), mangaMetadataRepository.subscribeTagsById(id), diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaMetadataRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaMetadataRepository.kt index e188bfbbe..145fa3db2 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaMetadataRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaMetadataRepository.kt @@ -11,15 +11,15 @@ import kotlinx.coroutines.flow.Flow interface MangaMetadataRepository { suspend fun getMetadataById(id: Long): SearchMetadata? - suspend fun subscribeMetadataById(id: Long): Flow + fun subscribeMetadataById(id: Long): Flow suspend fun getTagsById(id: Long): List - suspend fun subscribeTagsById(id: Long): Flow> + fun subscribeTagsById(id: Long): Flow> suspend fun getTitlesById(id: Long): List - suspend fun subscribeTitlesById(id: Long): Flow> + fun subscribeTitlesById(id: Long): Flow> suspend fun insertFlatMetadata(flatMetadata: FlatMetadata) diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt deleted file mode 100644 index c62acfd30..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt deleted file mode 100644 index 16a5f1c1d..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index f3e0dc5e4..70ad3aa59 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -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 */Pair/* SY <-- */>, - getMangaState: @Composable ((Manga) -> State), - // SY --> - getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), - // SY <-- + source: CatalogueSource?, + mangaList: LazyPagingItems */Pair/* 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, diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt deleted file mode 100644 index afff9f0dd..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt +++ /dev/null @@ -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> - 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> by derivedStateOf { filters.toItems() } - override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null) -} diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index e729f60ea..a52ef7d6c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt deleted file mode 100644 index 74db9d0bc..000000000 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt index 72e20d2ed..629807fc4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -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 */Pair/* SY <-- */>, - getMangaState: @Composable ((Manga) -> State), - // SY --> - getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), - // SY <-- + mangaList: LazyPagingItems */Pair/* 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, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt index ce35105ca..3d44fd524 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -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 */Pair/* SY <-- */>, - getMangaState: @Composable ((Manga) -> State), - // SY --> - getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), - // SY <-- + mangaList: LazyPagingItems */Pair/* 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, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt index 86b0fea67..af0047f3c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt @@ -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, + 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)) + } + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt index 86e86ad44..51fd5f8cb 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt @@ -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 */Pair/* SY <-- */>, - getMangaState: @Composable ((Manga) -> State), - // SY --> - getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), - // SY <-- + mangaList: LazyPagingItems */Pair/* 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, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt new file mode 100644 index 000000000..06fe7cfbc --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceFloatingActionButton.kt @@ -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, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt index 8627699db..7c8e09dfc 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -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 */Pair/* SY <-- */>, - getMangaState: @Composable ((Manga) -> State), - // SY --> - getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), - // SY <-- + mangaList: LazyPagingItems */Pair/* 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 --> diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index 408ed0a86..6b10ca024 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -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 = { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/SourceFeedDialogs.kt b/app/src/main/java/eu/kanade/presentation/browse/components/SourceFeedDialogs.kt index aea70f951..3ed5803df 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/SourceFeedDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/SourceFeedDialogs.kt @@ -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)) - }, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index d3738802f..8efa3b9a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -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().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() + .last() + .newSelectedItem = mangaId to it.id + navigator.popUntil { it is MigrationListScreen } // SY <-- }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt index a29277977..65aad17a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt new file mode 100644 index 000000000..725ff6bc2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -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() + .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) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt index d89c75a4c..1dad205c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index b0916de9b..a44e86278 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -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(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() @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) { - 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() - .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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt new file mode 100644 index 000000000..8220b8852 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -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() + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt similarity index 56% rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 74d138370..524ad877a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -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(), BrowseSourceState by state { +) : StateScreenModel(State(Filter.valueOf(searchQuery))) { private val loggedServices by lazy { Injekt.get().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 { - val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - return produceState(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 */Pair/* 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 { - 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 { - return produceState(initialValue = initialMetadata, manga.id) { - val source = source?.getMainSource>() ?: 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(jsonFilters) - filterSerializer.deserialize(this.filters, filters) - search(filters = this.filters) + val filtersJson = Json.decodeFromString(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 */Pair/* 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.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow> { + val metadataSource = source.getMainSource>() + 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() + .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() - .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) { - 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>, ) : 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) : 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> { 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> { group.subItems = subItems group } - is Filter.Sort -> { + is SourceModelFilter.Sort -> { val group = SortGroup(filter) val subItems = filter.values.map { SortItem(it, group) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 34f2e7466..a060d3a39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index 48eed0a69..f8a02fcc4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -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)) }, diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt index e3dbd95c3..c11e2d283 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt @@ -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" diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt deleted file mode 100644 index 7e64d539e..000000000 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt +++ /dev/null @@ -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 { - return remember { mutableStateOf(initialMetadata) } - } -} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt new file mode 100644 index 000000000..8e46e28cd --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreen.kt @@ -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) + } +} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt new file mode 100644 index 000000000..ab7f206a7 --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt @@ -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.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow> { + return map { it to metadata } + } + + override fun initFilterSheet(context: Context, router: Router) { + // No-op: we don't allow filtering in recs + } +} diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt index bba073ca2..bb108cb51 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt @@ -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" diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt deleted file mode 100644 index 25bee7ed8..000000000 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt +++ /dev/null @@ -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 { - return remember { mutableStateOf(initialMetadata) } - } -} diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt new file mode 100644 index 000000000..9a824b33f --- /dev/null +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreen.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt new file mode 100644 index 000000000..06d145fcd --- /dev/null +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt @@ -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.combineMetadata(dbManga: Manga, metadata: RaisedSearchMetadata?): Flow> { + return map { it to metadata } + } + + override fun initFilterSheet(context: Context, router: Router) { + // No-op: we don't allow filtering in recs + } +} diff --git a/app/src/main/java/exh/recs/RecommendsController.kt b/app/src/main/java/exh/recs/RecommendsController.kt index 3a3c38169..8891b2b62 100644 --- a/app/src/main/java/exh/recs/RecommendsController.kt +++ b/app/src/main/java/exh/recs/RecommendsController.kt @@ -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" diff --git a/app/src/main/java/exh/recs/RecommendsScreen.kt b/app/src/main/java/exh/recs/RecommendsScreen.kt new file mode 100644 index 000000000..e02a61022 --- /dev/null +++ b/app/src/main/java/exh/recs/RecommendsScreen.kt @@ -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, + ), + ), + ) + } +} diff --git a/app/src/main/java/exh/recs/RecommendsPresenter.kt b/app/src/main/java/exh/recs/RecommendsScreenModel.kt similarity index 56% rename from app/src/main/java/exh/recs/RecommendsPresenter.kt rename to app/src/main/java/exh/recs/RecommendsScreenModel.kt index 9039637b7..52e5ffcca 100644 --- a/app/src/main/java/exh/recs/RecommendsPresenter.kt +++ b/app/src/main/java/exh/recs/RecommendsScreenModel.kt @@ -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) } }