From 16ea8aa3b73d86ce9d841589806b50060384962f Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 31 Aug 2022 20:41:35 +0200 Subject: [PATCH] Use Compose on BrowseSourceScreens (#7901) (cherry picked from commit d4b764fa317ddcb04b8fefb982cd6b9a6dfc1598) # Conflicts: # app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.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/BrowseSourcePresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt # app/src/main/res/layout/source_comfortable_grid_item.xml # app/src/main/res/layout/source_compact_grid_item.xml # app/src/main/res/menu/source_browse.xml --- app/build.gradle.kts | 1 + .../kanade/data/manga/MangaRepositoryImpl.kt | 4 + .../domain/manga/interactor/GetManga.kt | 4 + .../manga/repository/MangaRepository.kt | 2 + .../presentation/browse/BrowseLatestScreen.kt | 82 ++ .../browse/BrowseMangadexFollowsScreen.kt | 86 ++ .../browse/BrowseRecommendationsScreen.kt | 73 ++ .../presentation/browse/BrowseSourceScreen.kt | 264 +++++++ .../presentation/browse/BrowseSourceState.kt | 37 + .../presentation/browse/SourceSearchScreen.kt | 28 + .../browse/components/BrowseLatestToolbar.kt | 119 +++ .../components/BrowseSourceComfortableGrid.kt | 151 ++++ .../components/BrowseSourceCompactGrid.kt | 174 +++++ .../browse/components/BrowseSourceDialogs.kt | 39 + .../components/BrowseSourceEHentaiList.kt | 265 +++++++ .../browse/components/BrowseSourceList.kt | 134 ++++ .../components/BrowseSourceLoadingItem.kt | 25 + .../components/BrowseSourceSimpleToolbar.kt | 86 ++ .../browse/components/BrowseSourceToolbar.kt | 227 ++++++ .../components/DuplicateMangaDialog.kt | 2 +- .../components/LibraryComfortableGrid.kt | 19 +- .../library/components/LibraryCompactGrid.kt | 38 +- .../library/components/LibraryGridCover.kt | 167 ++-- .../library/components/LibraryList.kt | 149 ++-- .../kanade/tachiyomi/data/cache/CoverCache.kt | 15 + .../search/SourceSearchController.kt | 42 +- .../source/browse/BrowsePagingSource.kt | 47 ++ .../source/browse/BrowseSourceController.kt | 738 +++--------------- .../source/browse/BrowseSourcePresenter.kt | 567 +++++++------- .../browse/EHentaiBrowsePagingSource.kt | 49 ++ .../ui/browse/source/browse/EHentaiPager.kt | 51 -- .../ui/browse/source/browse/Pager.kt | 38 - .../ui/browse/source/browse/ProgressItem.kt | 54 -- .../source/browse/SourceBrowsePagingSource.kt | 20 + .../browse/SourceComfortableGridHolder.kt | 71 -- .../source/browse/SourceCompactGridHolder.kt | 71 -- .../browse/SourceEnhancedEHentaiListHolder.kt | 108 --- .../ui/browse/source/browse/SourceHolder.kt | 40 - .../ui/browse/source/browse/SourceItem.kt | 85 -- .../browse/source/browse/SourceListHolder.kt | 77 -- .../ui/browse/source/browse/SourcePager.kt | 26 - .../browse/source/feed/SourceFeedPresenter.kt | 2 +- .../latest/LatestUpdatesBrowsePagingSource.kt | 13 + .../source/latest/LatestUpdatesController.kt | 78 +- .../source/latest/LatestUpdatesPager.kt | 13 - .../source/latest/LatestUpdatesPresenter.kt | 8 +- .../ui/library/ChangeMangaCategoriesDialog.kt | 81 -- .../ui/manga/AddDuplicateMangaDialog.kt | 48 -- .../tachiyomi/ui/manga/MangaController.kt | 2 +- .../kanade/tachiyomi/util/MangaExtensions.kt | 6 + .../md/follows/MangaDexFollowsController.kt | 79 +- .../exh/md/follows/MangaDexFollowsPager.kt | 14 - .../md/follows/MangaDexFollowsPagingSource.kt | 15 + .../md/follows/MangaDexFollowsPresenter.kt | 19 +- .../md/similar/MangaDexSimilarController.kt | 36 +- ...ager.kt => MangaDexSimilarPagingSource.kt} | 15 +- .../md/similar/MangaDexSimilarPresenter.kt | 23 +- .../java/exh/recs/RecommendsController.kt | 35 +- ...endsPager.kt => RecommendsPagingSource.kt} | 19 +- .../main/java/exh/recs/RecommendsPresenter.kt | 13 +- .../color/source_comfortable_item_title.xml | 6 - .../layout/source_comfortable_grid_item.xml | 78 -- .../res/layout/source_compact_grid_item.xml | 86 -- app/src/main/res/layout/source_controller.xml | 38 - .../source_enhanced_ehentai_list_item.xml | 117 --- app/src/main/res/layout/source_list_item.xml | 138 ---- .../main/res/layout/source_progress_item.xml | 27 - .../res/layout/source_recycler_autofit.xml | 11 - app/src/main/res/menu/source_browse.xml | 57 -- app/src/main/res/values/strings.xml | 1 + gradle/sy.versions.toml | 3 +- 71 files changed, 2820 insertions(+), 2536 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt create mode 100644 app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt rename app/src/main/java/eu/kanade/presentation/{manga => }/components/DuplicateMangaDialog.kt (97%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiBrowsePagingSource.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiPager.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/ProgressItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourcePager.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPager.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt delete mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsPagingSource.kt rename app/src/main/java/exh/md/similar/{MangaDexSimilarPager.kt => MangaDexSimilarPagingSource.kt} (65%) rename app/src/main/java/exh/recs/{RecommendsPager.kt => RecommendsPagingSource.kt} (95%) delete mode 100644 app/src/main/res/color/source_comfortable_item_title.xml delete mode 100644 app/src/main/res/layout/source_comfortable_grid_item.xml delete mode 100644 app/src/main/res/layout/source_compact_grid_item.xml delete mode 100644 app/src/main/res/layout/source_controller.xml delete mode 100644 app/src/main/res/layout/source_enhanced_ehentai_list_item.xml delete mode 100644 app/src/main/res/layout/source_list_item.xml delete mode 100644 app/src/main/res/layout/source_progress_item.xml delete mode 100644 app/src/main/res/layout/source_recycler_autofit.xml delete mode 100644 app/src/main/res/menu/source_browse.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 358bcccff..19573b115 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -281,6 +281,7 @@ dependencies { // RatingBar (SY) implementation(sylibs.ratingbar) + implementation(sylibs.composeRatingbar) } tasks { diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index addc26ffd..85ab47e08 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -29,6 +29,10 @@ class MangaRepositoryImpl( return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) } } + override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow { + return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) } + } + override suspend fun getFavorites(): List { return handler.awaitList { mangasQueries.getFavorites(mangaMapper) } } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetManga.kt index da787116b..e2c54b993 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/GetManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetManga.kt @@ -26,4 +26,8 @@ class GetManga( suspend fun await(url: String, sourceId: Long): Manga? { return mangaRepository.getMangaByUrlAndSourceId(url, sourceId) } + + fun subscribe(url: String, sourceId: Long): Flow { + return mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId) + } } diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt index 9a93f5b0e..57334c995 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt @@ -13,6 +13,8 @@ interface MangaRepository { suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? + fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow + suspend fun getFavorites(): List suspend fun getLibraryManga(): List diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt new file mode 100644 index 000000000..e37d88db6 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseLatestScreen.kt @@ -0,0 +1,82 @@ +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.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.paging.compose.collectAsLazyPagingItems +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.components.BrowseLatestToolbar +import eu.kanade.presentation.components.Scaffold +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter +import eu.kanade.tachiyomi.ui.more.MoreController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import exh.source.isEhBasedSource + +@Composable +fun BrowseLatestScreen( + presenter: BrowseSourcePresenter, + navigateUp: () -> Unit, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + val columns by presenter.getColumnsPreferenceForCurrentOrientation() + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val onHelpClick = { + uriHandler.openUri(LocalSource.HELP_URL) + } + + val onWebViewClick = f@{ + val source = presenter.source as? HttpSource ?: return@f + val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name) + context.startActivity(intent) + } + + Scaffold( + topBar = { + BrowseLatestToolbar( + navigateUp = navigateUp, + source = presenter.source!!, + displayMode = presenter.displayMode.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode }, + onDisplayModeChange = { presenter.displayMode = it }, + onHelpClick = onHelpClick, + onWebViewClick = onWebViewClick, + // SY --> + onSettingsClick = onSettingsClick, + // SY <-- + ) + }, + ) { paddingValues -> + BrowseSourceContent( + source = presenter.source, + mangaList = presenter.getMangaList().collectAsLazyPagingItems(), + getMangaState = { presenter.getManga(it) }, + // SY --> + getMetadataState = { manga, metadata -> + presenter.getRaisedSearchMetadata(manga, metadata) + }, + // SY <-- + columns = columns, + // SY --> + ehentaiBrowseDisplayMode = presenter.ehentaiBrowseDisplayMode, + // SY <-- + displayMode = presenter.displayMode, + snackbarHostState = remember { SnackbarHostState() }, + contentPadding = paddingValues, + onWebViewClick = onWebViewClick, + onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) }, + onLocalSourceHelpClick = onHelpClick, + onMangaClick = onMangaClick, + onMangaLongClick = onMangaLongClick, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt new file mode 100644 index 000000000..0da753475 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseMangadexFollowsScreen.kt @@ -0,0 +1,86 @@ +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.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +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.R +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter +import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode +import eu.kanade.tachiyomi.ui.more.MoreController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity + +@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() } + + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val onHelpClick = { + uriHandler.openUri(LocalSource.HELP_URL) + } + + val onWebViewClick = f@{ + val source = presenter.source as? HttpSource ?: return@f + val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name) + context.startActivity(intent) + } + + Scaffold( + topBar = { + BrowseSourceSimpleToolbar( + title = stringResource(R.string.mangadex_follows), + displayMode = presenter.displayMode, + onDisplayModeChange = onDisplayModeChange, + navigateUp = navigateUp, + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { paddingValues -> + BrowseSourceContent( + source = presenter.source, + 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, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt new file mode 100644 index 000000000..3cbf1e184 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseRecommendationsScreen.kt @@ -0,0 +1,73 @@ +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.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +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.source.LocalSource +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter +import eu.kanade.tachiyomi.ui.more.MoreController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity + +@Composable +fun BrowseRecommendationsScreen( + presenter: BrowseSourcePresenter, + navigateUp: () -> Unit, + title: String, + onMangaClick: (Manga) -> Unit, +) { + val columns by presenter.getColumnsPreferenceForCurrentOrientation() + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val onHelpClick = { + uriHandler.openUri(LocalSource.HELP_URL) + } + + val onWebViewClick = f@{ + val source = presenter.source as? HttpSource ?: return@f + val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name) + context.startActivity(intent) + } + + Scaffold( + topBar = { + BrowseSourceSimpleToolbar( + navigateUp = navigateUp, + title = title, + displayMode = presenter.displayMode, + onDisplayModeChange = { presenter.displayMode = it }, + ) + }, + ) { paddingValues -> + BrowseSourceContent( + source = presenter.source, + 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 = onWebViewClick, + onHelpClick = { uriHandler.openUri(MoreController.URL_HELP) }, + onLocalSourceHelpClick = onHelpClick, + 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 new file mode 100644 index 000000000..298969f44 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -0,0 +1,264 @@ +package eu.kanade.presentation.browse + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material3.Icon +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.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import eu.kanade.domain.manga.model.Manga +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.EmptyScreen +import eu.kanade.presentation.components.ExtendedFloatingActionButton +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.source.online.HttpSource +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter +import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException +import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode +import eu.kanade.tachiyomi.ui.more.MoreController +import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.widget.EmptyView +import exh.metadata.metadata.base.RaisedSearchMetadata +import exh.source.isEhBasedSource + +@Composable +fun BrowseSourceScreen( + presenter: BrowseSourcePresenter, + navigateUp: () -> Unit, + onDisplayModeChange: (LibraryDisplayMode) -> Unit, + onFabClick: () -> Unit, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + val columns by presenter.getColumnsPreferenceForCurrentOrientation() + + val mangaList = presenter.getMangaList().collectAsLazyPagingItems() + + val snackbarHostState = remember { SnackbarHostState() } + + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val onHelpClick = { + uriHandler.openUri(LocalSource.HELP_URL) + } + + val onWebViewClick = f@{ + val source = presenter.source as? HttpSource ?: return@f + val intent = WebViewActivity.newIntent(context, source.baseUrl, source.id, source.name) + context.startActivity(intent) + } + + Scaffold( + topBar = { + BrowseSourceToolbar( + state = presenter, + source = presenter.source!!, + displayMode = presenter.displayMode.takeUnless { presenter.source!!.isEhBasedSource() && presenter.ehentaiBrowseDisplayMode }, + onDisplayModeChange = onDisplayModeChange, + navigateUp = navigateUp, + onWebViewClick = onWebViewClick, + onHelpClick = onHelpClick, + onSearch = { presenter.search() }, + // SY --> + onSettingsClick = onSettingsClick, + // SY <-- + ) + }, + floatingActionButton = { + // SY --> + // if (presenter.filters.isNotEmpty()) { + ExtendedFloatingActionButton( + modifier = Modifier.navigationBarsPadding(), + text = { + Text( + text = if (presenter.filters.isNotEmpty()) { + stringResource(id = R.string.action_filter) + } else { + stringResource(R.string.saved_searches) + }, + ) + }, + icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") }, + onClick = onFabClick, + ) + // } + // SY <-- + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { paddingValues -> + BrowseSourceContent( + source = presenter.source, + 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 BrowseSourceContent( + source: CatalogueSource?, + mangaList: LazyPagingItems */Pair/* SY <-- */>, + getMangaState: @Composable ((Manga) -> State), + // SY --> + getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), + // SY <-- + columns: GridCells, + ehentaiBrowseDisplayMode: Boolean, + displayMode: LibraryDisplayMode, + snackbarHostState: SnackbarHostState, + contentPadding: PaddingValues, + onWebViewClick: () -> Unit, + onHelpClick: () -> Unit, + onLocalSourceHelpClick: () -> Unit, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, +) { + val context = LocalContext.current + + val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error } + ?: mangaList.loadState.append.takeIf { it is LoadState.Error } + + val getErrorMessage: (LoadState.Error) -> String = { state -> + when { + state.error is NoResultsException -> context.getString(R.string.no_results_found) + state.error.message == null -> "" + state.error.message!!.startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}" + else -> state.error.message!! + } + } + + LaunchedEffect(errorState) { + if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { + val result = snackbarHostState.showSnackbar( + message = getErrorMessage(errorState), + actionLabel = context.getString(R.string.action_webview_refresh), + duration = SnackbarDuration.Indefinite, + ) + when (result) { + SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() + SnackbarResult.ActionPerformed -> mangaList.refresh() + } + } + } + + if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { + EmptyScreen( + message = getErrorMessage(errorState), + actions = if (source is LocalSource) { + listOf( + EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { onLocalSourceHelpClick() }, + ) + } else { + listOf( + EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp) { mangaList.refresh() }, + EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { onWebViewClick() }, + EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { onHelpClick() }, + ) + }, + ) + + return + } + + // SY --> + if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) { + BrowseSourceEHentaiList( + mangaList = mangaList, + getMangaState = getMangaState, + getMetadataState = getMetadataState, + contentPadding = contentPadding, + onMangaClick = onMangaClick, + onMangaLongClick = onMangaLongClick, + ) + return + } + // SY <-- + + when (displayMode) { + LibraryDisplayMode.ComfortableGrid -> { + BrowseSourceComfortableGrid( + mangaList = mangaList, + getMangaState = getMangaState, + // SY --> + getMetadataState = getMetadataState, + // SY <-- + columns = columns, + contentPadding = contentPadding, + onMangaClick = onMangaClick, + onMangaLongClick = onMangaLongClick, + ) + } + LibraryDisplayMode.List -> { + BrowseSourceList( + mangaList = mangaList, + getMangaState = getMangaState, + // SY --> + getMetadataState = getMetadataState, + // SY <-- + contentPadding = contentPadding, + onMangaClick = onMangaClick, + onMangaLongClick = onMangaLongClick, + ) + } + else -> { + BrowseSourceCompactGrid( + mangaList = mangaList, + getMangaState = getMangaState, + // SY --> + getMetadataState = getMetadataState, + // SY <-- + columns = columns, + contentPadding = contentPadding, + onMangaClick = onMangaClick, + onMangaLongClick = onMangaLongClick, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt new file mode 100644 index 000000000..45a9fa4ce --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceState.kt @@ -0,0 +1,37 @@ +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.toItems + +@Stable +interface BrowseSourceState { + val source: CatalogueSource? + var searchQuery: String? + val currentQuery: String + val filters: FilterList + val filterItems: List> + val appliedFilters: FilterList + var dialog: BrowseSourcePresenter.Dialog? +} + +fun BrowseSourceState(initialQuery: String?): BrowseSourceState { + return BrowseSourceStateImpl(initialQuery) +} + +class BrowseSourceStateImpl(initialQuery: String?) : BrowseSourceState { + override var source: CatalogueSource? by mutableStateOf(null) + override var searchQuery: String? by mutableStateOf(initialQuery) + override var currentQuery: String by mutableStateOf(initialQuery ?: "") + override var filters: FilterList by mutableStateOf(FilterList()) + override val filterItems: List> by derivedStateOf { filters.toItems() } + override var appliedFilters by mutableStateOf(FilterList()) + override var dialog: BrowseSourcePresenter.Dialog? by mutableStateOf(null) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt new file mode 100644 index 000000000..9834fb582 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceSearchScreen.kt @@ -0,0 +1,28 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.Composable +import eu.kanade.domain.manga.model.Manga +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter + +@Composable +fun SourceSearchScreen( + presenter: BrowseSourcePresenter, + navigateUp: () -> Unit, + onFabClick: () -> Unit, + onClickManga: (Manga) -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + BrowseSourceScreen( + presenter = presenter, + navigateUp = navigateUp, + onDisplayModeChange = { presenter.displayMode = (it) }, + onFabClick = onFabClick, + onMangaClick = onClickManga, + onMangaLongClick = onClickManga, + // SY --> + onSettingsClick = onSettingsClick, + // SY <-- + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt new file mode 100644 index 000000000..deefe959f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseLatestToolbar.kt @@ -0,0 +1,119 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ViewModule +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Help +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode +import exh.source.anyIs + +@Composable +fun BrowseLatestToolbar( + navigateUp: () -> Unit, + source: CatalogueSource, + displayMode: LibraryDisplayMode?, + onDisplayModeChange: (LibraryDisplayMode) -> Unit, + onHelpClick: () -> Unit, + onWebViewClick: () -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + AppBar( + navigateUp = navigateUp, + title = source.name, + actions = { + var selectingDisplayMode by remember { mutableStateOf(false) } + AppBarActions( + // SY --> + actions = listOfNotNull( + AppBar.Action( + title = "display_mode", + icon = Icons.Filled.ViewModule, + onClick = { selectingDisplayMode = true }, + ).takeIf { displayMode != null }, + // SY <-- + if (source is LocalSource) { + AppBar.Action( + title = "help", + icon = Icons.Outlined.Help, + onClick = onHelpClick, + ) + } else { + AppBar.Action( + title = stringResource(R.string.action_web_view), + icon = Icons.Outlined.Public, + onClick = onWebViewClick, + ) + }, + // SY --> + AppBar.Action( + title = stringResource(R.string.action_settings), + icon = Icons.Outlined.Settings, + onClick = onSettingsClick, + ).takeIf { source.anyIs() }, + // SY <-- + ), + ) + DropdownMenu( + expanded = selectingDisplayMode, + onDismissRequest = { selectingDisplayMode = false }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.ComfortableGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.CompactGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_list)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.List) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.List) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + } + }, + ) +} 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 new file mode 100644 index 000000000..004c4bfb5 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -0,0 +1,151 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.grid.GridCells +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.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.Badge +import eu.kanade.presentation.components.MangaCover +import eu.kanade.presentation.library.components.MangaGridComfortableText +import eu.kanade.presentation.library.components.MangaGridCover +import eu.kanade.presentation.util.plus +import eu.kanade.tachiyomi.R +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata + +@Composable +fun BrowseSourceComfortableGrid( + mangaList: LazyPagingItems */Pair/* SY <-- */>, + getMangaState: @Composable ((Manga) -> State), + // SY --> + getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), + // SY <-- + columns: GridCells, + contentPadding: PaddingValues, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, +) { + LazyVerticalGrid( + columns = columns, + contentPadding = PaddingValues(8.dp) + contentPadding, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + if (mangaList.loadState.prepend is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + + 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) + // SY <-- + BrowseSourceComfortableGridItem( + manga = manga, + // SY --> + metadata = metadata, + // SY <-- + onClick = { onMangaClick(manga) }, + onLongClick = { onMangaLongClick(manga) }, + ) + } + + item(span = { GridItemSpan(maxLineSpan) }) { + if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + } +} + +@Composable +fun BrowseSourceComfortableGridItem( + manga: Manga, + // SY --> + metadata: RaisedSearchMetadata?, + // SY <-- + onClick: () -> Unit = {}, + onLongClick: () -> Unit = onClick, +) { + val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f) + Column( + modifier = Modifier + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), + ) { + MangaGridCover( + cover = { + MangaCover.Book( + modifier = Modifier + .fillMaxWidth() + .drawWithContent { + drawContent() + if (manga.favorite) { + drawRect(overlayColor) + } + }, + data = manga.thumbnailUrl, + ) + }, + badgesStart = { + if (manga.favorite) { + Badge(text = stringResource(id = R.string.in_library)) + } + }, + // SY --> + badgesEnd = { + if (metadata is MangaDexSearchMetadata) { + metadata.followStatus?.let { followStatus -> + val text = LocalContext.current + .resources + .let { + remember { + it.getStringArray(R.array.md_follows_options) + .getOrNull(followStatus) + } + } + ?: return@let + Badge( + text = text, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + metadata.relation?.let { + Badge( + text = stringResource(it.resId), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + } + }, + // SY <-- + ) + MangaGridComfortableText( + text = manga.title, + ) + } +} 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 new file mode 100644 index 000000000..9bf37e215 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -0,0 +1,174 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.Badge +import eu.kanade.presentation.components.MangaCover +import eu.kanade.presentation.library.components.MangaGridCompactText +import eu.kanade.presentation.library.components.MangaGridCover +import eu.kanade.presentation.util.plus +import eu.kanade.tachiyomi.R +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata + +@Composable +fun BrowseSourceCompactGrid( + mangaList: LazyPagingItems */Pair/* SY <-- */>, + getMangaState: @Composable ((Manga) -> State), + // SY --> + getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), + // SY <-- + columns: GridCells, + contentPadding: PaddingValues, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, +) { + LazyVerticalGrid( + columns = columns, + contentPadding = PaddingValues(8.dp) + contentPadding, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + if (mangaList.loadState.prepend is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + + 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) + // SY <-- + BrowseSourceCompactGridItem( + manga = manga, + // SY --> + metadata = metadata, + // SY <-- + onClick = { onMangaClick(manga) }, + onLongClick = { onMangaLongClick(manga) }, + ) + } + + item(span = { GridItemSpan(maxLineSpan) }) { + if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + } +} + +@Composable +fun BrowseSourceCompactGridItem( + manga: Manga, + // SY --> + metadata: RaisedSearchMetadata?, + // SY <-- + onClick: () -> Unit = {}, + onLongClick: () -> Unit = onClick, +) { + val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f) + MangaGridCover( + modifier = Modifier + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), + cover = { + MangaCover.Book( + modifier = Modifier + .fillMaxHeight() + .drawWithContent { + drawContent() + if (manga.favorite) { + drawRect(overlayColor) + } + }, + data = eu.kanade.domain.manga.model.MangaCover( + manga.id, + manga.source, + manga.favorite, + manga.thumbnailUrl, + manga.coverLastModified, + ), + ) + }, + badgesStart = { + if (manga.favorite) { + Badge(text = stringResource(id = R.string.in_library)) + } + }, + // SY --> + badgesEnd = { + if (metadata is MangaDexSearchMetadata) { + metadata.followStatus?.let { followStatus -> + val text = LocalContext.current + .resources + .let { + remember { + it.getStringArray(R.array.md_follows_options) + .getOrNull(followStatus) + } + } + ?: return@let + Badge( + text = text, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + metadata.relation?.let { + Badge( + text = stringResource(it.resId), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + } + }, + // SY <-- + content = { + Box( + modifier = Modifier + .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp)) + .background( + Brush.verticalGradient( + 0f to Color.Transparent, + 1f to Color(0xAA000000), + ), + ) + .fillMaxHeight(0.33f) + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + MangaGridCompactText(manga.title) + }, + ) +} 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 new file mode 100644 index 000000000..83f1e5528 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt @@ -0,0 +1,39 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.material.TextButton +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R + +@Composable +fun RemoveMangaDialog( + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onDismissRequest() + onConfirm() + }, + ) { + Text(text = stringResource(id = R.string.action_remove)) + } + }, + title = { + Text(text = stringResource(id = R.string.are_you_sure)) + }, + text = { + Text(text = stringResource(R.string.remove_manga)) + }, + ) +} 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 new file mode 100644 index 000000000..11a8d9603 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceEHentaiList.kt @@ -0,0 +1,265 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +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.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.items +import com.gowtham.ratingbar.RatingBar +import com.gowtham.ratingbar.RatingBarConfig +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.Badge +import eu.kanade.presentation.components.BadgeGroup +import eu.kanade.presentation.components.LazyColumn +import eu.kanade.presentation.components.MangaCover +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.lang.withIOContext +import exh.metadata.MetadataUtil +import exh.metadata.metadata.EHentaiSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata +import exh.util.SourceTagsUtil +import exh.util.SourceTagsUtil.GenreColor +import exh.util.floor +import java.util.Date + +@Composable +fun BrowseSourceEHentaiList( + mangaList: LazyPagingItems */Pair/* SY <-- */>, + getMangaState: @Composable ((Manga) -> State), + // SY --> + getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), + // SY <-- + contentPadding: PaddingValues, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, +) { + LazyColumn( + contentPadding = contentPadding, + ) { + item { + if (mangaList.loadState.prepend is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + + items(mangaList) { initialManga -> + initialManga ?: return@items + val manga by getMangaState(initialManga.first) + // SY --> + val metadata by getMetadataState(initialManga.first, initialManga.second) + // SY <-- + BrowseSourceEHentaiListItem( + manga = manga, + // SY --> + metadata = metadata, + // SY <-- + onClick = { onMangaClick(manga) }, + onLongClick = { onMangaLongClick(manga) }, + ) + } + + item { + if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + } +} + +@Composable +fun BrowseSourceEHentaiListItem( + manga: Manga, + // SY --> + metadata: RaisedSearchMetadata?, + // SY <-- + onClick: () -> Unit = {}, + onLongClick: () -> Unit = onClick, +) { + if (metadata !is EHentaiSearchMetadata) return + val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f) + + val resources = LocalContext.current.resources + val languageText by produceState("", metadata) { + value = withIOContext { + val locale = SourceTagsUtil.getLocaleSourceUtil( + metadata.tags + .firstOrNull { it.namespace == EHentaiSearchMetadata.EH_LANGUAGE_NAMESPACE } + ?.name, + ) + val pageCount = metadata.length + if (locale != null && pageCount != null) { + resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().uppercase()) + } else if (pageCount != null) { + resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount) + } else locale?.toLanguageTag()?.uppercase().orEmpty() + } + } + val datePosted by produceState("", metadata) { + value = withIOContext { + runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } } + .getOrNull() + .orEmpty() + } + } + val genre by produceState?>(null, metadata) { + value = withIOContext { + when (metadata.genre) { + "doujinshi" -> GenreColor.DOUJINSHI_COLOR to R.string.doujinshi + "manga" -> GenreColor.MANGA_COLOR to R.string.manga + "artistcg" -> GenreColor.ARTIST_CG_COLOR to R.string.artist_cg + "gamecg" -> GenreColor.GAME_CG_COLOR to R.string.game_cg + "western" -> GenreColor.WESTERN_COLOR to R.string.western + "non-h" -> GenreColor.NON_H_COLOR to R.string.non_h + "imageset" -> GenreColor.IMAGE_SET_COLOR to R.string.image_set + "cosplay" -> GenreColor.COSPLAY_COLOR to R.string.cosplay + "asianporn" -> GenreColor.ASIAN_PORN_COLOR to R.string.asian_porn + "misc" -> GenreColor.MISC_COLOR to R.string.misc + else -> null + } + } + } + val rating by produceState(0f, metadata) { + value = withIOContext { + val rating = metadata.averageRating?.toFloat() + rating?.div(0.5F)?.floor()?.let { 0.5F.times(it) } ?: 0f + } + } + + Row( + modifier = Modifier + .height(148.dp) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box { + MangaCover.Book( + modifier = Modifier + .fillMaxHeight() + .drawWithContent { + drawContent() + if (manga.favorite) { + drawRect(overlayColor) + } + }, + data = manga.thumbnailUrl, + ) + if (manga.favorite) { + BadgeGroup( + modifier = Modifier + .padding(4.dp) + .align(Alignment.TopStart), + ) { + Badge(stringResource(R.string.in_library)) + } + } + } + Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween) { + Column(Modifier.fillMaxWidth()) { + Text( + text = manga.title, + maxLines = 2, + modifier = Modifier.padding(start = 8.dp, top = 8.dp), + style = MaterialTheme.typography.titleSmall, + overflow = TextOverflow.Ellipsis, + ) + metadata.uploader?.let { + Text( + text = it, + maxLines = 1, + modifier = Modifier.padding(start = 8.dp), + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp, + ) + } + } + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, start = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + ) { + RatingBar( + value = rating, + onValueChange = {}, + onRatingChanged = {}, + config = RatingBarConfig().apply { + isIndicator(true) + numStars(5) + size(18.dp) + activeColor(Color(0xFF005ED7)) + inactiveColor(Color(0xE1E2ECFF)) + }, + ) + val color = genre?.first?.color + val res = genre?.second + Card( + colors = if (color != null) { + CardDefaults.cardColors(Color(color)) + } else CardDefaults.cardColors(), + ) { + Text( + text = if (res != null) { + stringResource(res) + } else { + metadata.genre.orEmpty() + }, + modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), + maxLines = 1, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.End, + ) { + Text( + languageText, + maxLines = 1, + fontSize = 14.sp, + ) + Text( + datePosted, + maxLines = 1, + fontSize = 14.sp, + ) + } + } + } + } +} 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 new file mode 100644 index 000000000..599ecce5b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -0,0 +1,134 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.items +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.Badge +import eu.kanade.presentation.components.LazyColumn +import eu.kanade.presentation.components.MangaCover +import eu.kanade.presentation.library.components.MangaListItem +import eu.kanade.presentation.library.components.MangaListItemContent +import eu.kanade.presentation.util.verticalPadding +import eu.kanade.tachiyomi.R +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata + +@Composable +fun BrowseSourceList( + mangaList: LazyPagingItems */Pair/* SY <-- */>, + getMangaState: @Composable ((Manga) -> State), + // SY --> + getMetadataState: @Composable ((Manga, RaisedSearchMetadata?) -> State), + // SY <-- + contentPadding: PaddingValues, + onMangaClick: (Manga) -> Unit, + onMangaLongClick: (Manga) -> Unit, +) { + LazyColumn( + contentPadding = contentPadding, + ) { + item { + if (mangaList.loadState.prepend is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + + items(mangaList) { initialManga -> + initialManga ?: return@items + val manga by getMangaState(initialManga.first) + // SY --> + val metadata by getMetadataState(initialManga.first, initialManga.second) + // SY <-- + BrowseSourceListItem( + manga = manga, + // SY --> + metadata = metadata, + // SY <-- + onClick = { onMangaClick(manga) }, + onLongClick = { onMangaLongClick(manga) }, + ) + } + + item { + if (mangaList.loadState.refresh is LoadState.Loading || mangaList.loadState.append is LoadState.Loading) { + BrowseSourceLoadingItem() + } + } + } +} + +@Composable +fun BrowseSourceListItem( + manga: Manga, + // SY --> + metadata: RaisedSearchMetadata?, + // SY <-- + onClick: () -> Unit = {}, + onLongClick: () -> Unit = onClick, +) { + val overlayColor = MaterialTheme.colorScheme.background.copy(alpha = 0.66f) + MangaListItem( + coverContent = { + MangaCover.Square( + modifier = Modifier + .padding(vertical = verticalPadding) + .fillMaxHeight() + .drawWithContent { + drawContent() + if (manga.favorite) { + drawRect(overlayColor) + } + }, + data = manga.thumbnailUrl, + ) + }, + onClick = onClick, + onLongClick = onLongClick, + badges = { + if (manga.favorite) { + Badge(text = stringResource(id = R.string.in_library)) + } + if (metadata is MangaDexSearchMetadata) { + metadata.followStatus?.let { followStatus -> + val text = LocalContext.current + .resources + .let { + remember { + it.getStringArray(R.array.md_follows_options) + .getOrNull(followStatus) + } + } + ?: return@let + Badge( + text = text, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + metadata.relation?.let { + Badge( + text = stringResource(it.resId), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + } + }, + content = { + MangaListItemContent(text = manga.title) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt new file mode 100644 index 000000000..d27fadf4d --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceLoadingItem.kt @@ -0,0 +1,25 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun BrowseSourceLoadingItem() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt new file mode 100644 index 000000000..856948b61 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceSimpleToolbar.kt @@ -0,0 +1,86 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ViewModule +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode + +@Composable +fun BrowseSourceSimpleToolbar( + navigateUp: () -> Unit, + title: String, + displayMode: LibraryDisplayMode?, + onDisplayModeChange: (LibraryDisplayMode) -> Unit, +) { + AppBar( + navigateUp = navigateUp, + title = title, + actions = { + var selectingDisplayMode by remember { mutableStateOf(false) } + AppBarActions( + // SY --> + actions = listOfNotNull( + AppBar.Action( + title = "display_mode", + icon = Icons.Filled.ViewModule, + onClick = { selectingDisplayMode = true }, + ), + ), + ) + DropdownMenu( + expanded = selectingDisplayMode, + onDismissRequest = { selectingDisplayMode = false }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.ComfortableGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.CompactGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_list)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.List) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.List) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + } + }, + ) +} 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 new file mode 100644 index 000000000..92aff1f7b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -0,0 +1,227 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ViewModule +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Help +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import eu.kanade.presentation.browse.BrowseSourceState +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode +import exh.source.anyIs +import kotlinx.coroutines.delay + +@Composable +fun BrowseSourceToolbar( + state: BrowseSourceState, + source: CatalogueSource, + displayMode: LibraryDisplayMode?, + onDisplayModeChange: (LibraryDisplayMode) -> Unit, + navigateUp: () -> Unit, + onWebViewClick: () -> Unit, + onHelpClick: () -> Unit, + onSearch: () -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + if (state.searchQuery == null) { + BrowseSourceRegularToolbar( + source = source, + displayMode = displayMode, + onDisplayModeChange = onDisplayModeChange, + navigateUp = navigateUp, + onSearchClick = { state.searchQuery = "" }, + onWebViewClick = onWebViewClick, + onHelpClick = onHelpClick, + // SY --> + onSettingsClick = onSettingsClick, + // SY <-- + ) + } else { + BrowseSourceSearchToolbar( + searchQuery = state.searchQuery!!, + onSearchQueryChanged = { state.searchQuery = it }, + navigateUp = { + state.searchQuery = null + onSearch() + }, + onResetClick = { state.searchQuery = "" }, + onSearchClick = onSearch, + ) + } +} + +@Composable +fun BrowseSourceRegularToolbar( + source: CatalogueSource, + displayMode: LibraryDisplayMode?, + onDisplayModeChange: (LibraryDisplayMode) -> Unit, + navigateUp: () -> Unit, + onSearchClick: () -> Unit, + onWebViewClick: () -> Unit, + onHelpClick: () -> Unit, + // SY --> + onSettingsClick: () -> Unit, + // SY <-- +) { + AppBar( + navigateUp = navigateUp, + title = source.name, + actions = { + var selectingDisplayMode by remember { mutableStateOf(false) } + AppBarActions( + actions = listOfNotNull( + AppBar.Action( + title = "search", + icon = Icons.Outlined.Search, + onClick = onSearchClick, + ), + // SY --> + AppBar.Action( + title = "display_mode", + icon = Icons.Filled.ViewModule, + onClick = { selectingDisplayMode = true }, + ).takeIf { displayMode != null }, + // SY <-- + if (source is LocalSource) { + AppBar.Action( + title = "help", + icon = Icons.Outlined.Help, + onClick = onHelpClick, + ) + } else { + AppBar.Action( + title = stringResource(R.string.action_web_view), + icon = Icons.Outlined.Public, + onClick = onWebViewClick, + ) + }, + // SY --> + AppBar.Action( + title = stringResource(R.string.action_settings), + icon = Icons.Outlined.Settings, + onClick = onSettingsClick, + ).takeIf { source.anyIs() }, + // SY <-- + ), + ) + DropdownMenu( + expanded = selectingDisplayMode, + onDismissRequest = { selectingDisplayMode = false }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_comfortable_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.ComfortableGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.ComfortableGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_grid)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.CompactGrid) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.CompactGrid) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_display_list)) }, + onClick = { onDisplayModeChange(LibraryDisplayMode.List) }, + trailingIcon = { + if (displayMode == LibraryDisplayMode.List) { + Icon( + imageVector = Icons.Outlined.Check, + contentDescription = "", + ) + } + }, + ) + } + }, + ) +} + +@Composable +fun BrowseSourceSearchToolbar( + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + navigateUp: () -> Unit, + onResetClick: () -> Unit, + onSearchClick: () -> Unit, +) { + val focusRequester = remember { FocusRequester() } + AppBar( + navigateUp = navigateUp, + titleContent = { + BasicTextField( + value = searchQuery, + onValueChange = onSearchQueryChanged, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + onSearchClick() + }, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface), + ) + }, + actions = { + AppBarActions( + actions = listOf( + AppBar.Action( + title = "clear", + icon = Icons.Outlined.Clear, + onClick = onResetClick, + ), + ), + ) + }, + ) + LaunchedEffect(Unit) { + // TODO: https://issuetracker.google.com/issues/204502668 + delay(100) + focusRequester.requestFocus() + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/components/DuplicateMangaDialog.kt similarity index 97% rename from app/src/main/java/eu/kanade/presentation/manga/components/DuplicateMangaDialog.kt rename to app/src/main/java/eu/kanade/presentation/components/DuplicateMangaDialog.kt index 5b8e7f39c..a39a378ec 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DuplicateMangaDialog.kt @@ -1,4 +1,4 @@ -package eu.kanade.presentation.manga.components +package eu.kanade.presentation.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt index 625d6071e..84eded436 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt @@ -91,13 +91,22 @@ fun LibraryComfortableGridItem( }, // SY <-- ) - Text( - modifier = Modifier.padding(4.dp), + MangaGridComfortableText( text = manga.title, - fontSize = 12.sp, - maxLines = 2, - style = MaterialTheme.typography.titleSmall, ) } } } + +@Composable +fun MangaGridComfortableText( + text: String, +) { + Text( + modifier = Modifier.padding(4.dp), + text = text, + fontSize = 12.sp, + maxLines = 2, + style = MaterialTheme.typography.titleSmall, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt index 18ec8ae4f..9dc174e15 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -113,20 +114,27 @@ fun LibraryCompactGridItem( .fillMaxWidth() .align(Alignment.BottomCenter), ) - Text( - text = manga.title, - modifier = Modifier - .padding(8.dp) - .align(Alignment.BottomStart), - color = Color.White, - fontSize = 12.sp, - maxLines = 2, - style = MaterialTheme.typography.titleSmall.copy( - shadow = Shadow( - color = Color.Black, - blurRadius = 4f, - ), - ), - ) + MangaGridCompactText(manga.title) } } + +@Composable +fun BoxScope.MangaGridCompactText( + text: String, +) { + Text( + text = text, + modifier = Modifier + .padding(8.dp) + .align(Alignment.BottomStart), + color = Color.White, + fontSize = 12.sp, + maxLines = 2, + style = MaterialTheme.typography.titleSmall.copy( + shadow = Shadow( + color = Color.Black, + blurRadius = 4f, + ), + ), + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt index 7e0ffc9f7..4fd088fd5 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -17,6 +18,64 @@ import eu.kanade.presentation.components.BadgeGroup import eu.kanade.presentation.components.MangaCover import eu.kanade.tachiyomi.R +@Composable +fun MangaGridCover( + modifier: Modifier = Modifier, + cover: @Composable BoxScope.() -> Unit = {}, + badgesStart: (@Composable RowScope.() -> Unit)? = null, + badgesEnd: (@Composable RowScope.() -> Unit)? = null, + // SY --> + showPlayButton: Boolean = false, + playButtonPosition: PlayButtonPosition = PlayButtonPosition.Bottom, + onOpenReader: () -> Unit = {}, + // SY <-- + content: @Composable BoxScope.() -> Unit = {}, +) { + Box( + modifier = modifier + .fillMaxWidth() + .aspectRatio(MangaCover.Book.ratio), + ) { + cover() + content() + if (badgesStart != null) { + BadgeGroup( + modifier = Modifier + .padding(4.dp) + .align(Alignment.TopStart), + content = badgesStart, + ) + } + + // SY --> + Column( + Modifier + .align(Alignment.TopEnd), + horizontalAlignment = Alignment.End, + ) { + // SY <-- + if (badgesEnd != null) { + BadgeGroup( + modifier = Modifier + .padding(4.dp), + content = badgesEnd, + ) + } + // SY --> + if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) { + StartReadingButton(onOpenReader = onOpenReader) + } + } + if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) { + StartReadingButton( + Modifier.align(playButtonPosition.alignment), + onOpenReader = onOpenReader, + ) + } + // SY <-- + } +} + @Composable fun LibraryGridCover( modifier: Modifier = Modifier, @@ -32,78 +91,48 @@ fun LibraryGridCover( // SY <-- content: @Composable BoxScope.() -> Unit = {}, ) { - Box( - modifier = modifier - .fillMaxWidth() - .aspectRatio(MangaCover.Book.ratio), - ) { - MangaCover.Book( - modifier = Modifier.fillMaxWidth(), - data = mangaCover, - ) - content() - if (downloadCount > 0 || unreadCount > 0) { - BadgeGroup( - modifier = Modifier - .padding(4.dp) - .align(Alignment.TopStart), - ) { - if (downloadCount > 0) { - Badge( - text = "$downloadCount", - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } - if (unreadCount > 0) { - Badge(text = "$unreadCount") - } - } - } - if (isLocal || language.isNotEmpty()) { - // SY --> - Column( - Modifier.align(Alignment.TopEnd), - horizontalAlignment = Alignment.End, - ) { - // SY <-- - BadgeGroup( - modifier = Modifier - .padding(4.dp), - ) { - if (isLocal) { - Badge( - text = stringResource(R.string.local_source_badge), - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } else if (language.isNotEmpty()) { - Badge( - text = language, - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } - } - // SY --> - if (showPlayButton && playButtonPosition == PlayButtonPosition.Top) { - StartReadingButton(onOpenReader = onOpenReader) - } - } - if (showPlayButton && playButtonPosition == PlayButtonPosition.Bottom) { - StartReadingButton( - Modifier.align(playButtonPosition.alignment), - onOpenReader = onOpenReader, + MangaGridCover( + modifier = modifier, + cover = { + MangaCover.Book( + modifier = Modifier.fillMaxWidth(), + data = mangaCover, + ) + }, + badgesStart = { + if (downloadCount > 0) { + Badge( + text = "$downloadCount", + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, ) } - } else if (showPlayButton) { - StartReadingButton( - modifier = Modifier.align(playButtonPosition.alignment), - onOpenReader = onOpenReader, - ) - } + if (unreadCount > 0) { + Badge(text = "$unreadCount") + } + }, + badgesEnd = { + if (isLocal) { + Badge( + text = stringResource(R.string.local_source_badge), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } else if (language.isNotEmpty()) { + Badge( + text = language, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + }, + // SY --> + showPlayButton = showPlayButton, + playButtonPosition = playButtonPosition, + onOpenReader = onOpenReader, // SY <-- - } + content = content, + ) } enum class PlayButtonPosition(val alignment: Alignment) { diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt index 8f35b4344..4ddc9aaf4 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt @@ -2,6 +2,7 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -19,6 +20,7 @@ import eu.kanade.domain.manga.model.MangaCover import eu.kanade.presentation.components.Badge import eu.kanade.presentation.components.BadgeGroup import eu.kanade.presentation.components.FastScrollLazyColumn +import eu.kanade.presentation.components.MangaCover.Square import eu.kanade.presentation.components.TextButton import eu.kanade.presentation.util.bottomNavPaddingValues import eu.kanade.presentation.util.horizontalPadding @@ -74,62 +76,109 @@ fun LibraryListItem( onLongClick: (LibraryManga) -> Unit, ) { val manga = item.manga + MangaListItem( + modifier = Modifier.selectedBackground(isSelected), + title = manga.title, + cover = MangaCover( + manga.id!!, + manga.source, + manga.favorite, + manga.thumbnail_url, + manga.cover_last_modified, + ), + onClick = { onClick(manga) }, + onLongClick = { onLongClick(manga) }, + ) { + if (item.downloadCount > 0) { + Badge( + text = "${item.downloadCount}", + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + if (item.unreadCount > 0) { + Badge(text = "${item.unreadCount}") + } + if (item.isLocal) { + Badge( + text = stringResource(R.string.local_source_badge), + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) { + Badge( + text = item.sourceLanguage, + color = MaterialTheme.colorScheme.tertiary, + textColor = MaterialTheme.colorScheme.onTertiary, + ) + } + } +} + +@Composable +fun MangaListItem( + modifier: Modifier = Modifier, + title: String, + cover: MangaCover, + onClick: () -> Unit, + onLongClick: () -> Unit = onClick, + badges: @Composable RowScope.() -> Unit, +) { + MangaListItem( + modifier = modifier, + coverContent = { + Square( + modifier = Modifier + .padding(vertical = verticalPadding) + .fillMaxHeight(), + data = cover, + ) + }, + badges = badges, + onClick = onClick, + onLongClick = onLongClick, + content = { + MangaListItemContent(title) + }, + ) +} + +@Composable +fun MangaListItem( + modifier: Modifier = Modifier, + coverContent: @Composable RowScope.() -> Unit, + badges: @Composable RowScope.() -> Unit, + onClick: () -> Unit, + onLongClick: () -> Unit, + content: @Composable RowScope.() -> Unit, +) { Row( - modifier = Modifier - .selectedBackground(isSelected) + modifier = modifier .height(56.dp) .combinedClickable( - onClick = { onClick(manga) }, - onLongClick = { onLongClick(manga) }, + onClick = onClick, + onLongClick = onLongClick, ) .padding(horizontal = horizontalPadding), verticalAlignment = Alignment.CenterVertically, ) { - eu.kanade.presentation.components.MangaCover.Square( - modifier = Modifier - .padding(vertical = verticalPadding) - .fillMaxHeight(), - data = MangaCover( - manga.id!!, - manga.source, - manga.favorite, - manga.thumbnail_url, - manga.cover_last_modified, - ), - ) - Text( - text = manga.title, - modifier = Modifier - .padding(horizontal = horizontalPadding) - .weight(1f), - maxLines = 2, - style = MaterialTheme.typography.bodyMedium, - ) - BadgeGroup { - if (item.downloadCount > 0) { - Badge( - text = "${item.downloadCount}", - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } - if (item.unreadCount > 0) { - Badge(text = "${item.unreadCount}") - } - if (item.isLocal) { - Badge( - text = stringResource(R.string.local_source_badge), - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } - if (item.isLocal.not() && item.sourceLanguage.isNotEmpty()) { - Badge( - text = item.sourceLanguage, - color = MaterialTheme.colorScheme.tertiary, - textColor = MaterialTheme.colorScheme.onTertiary, - ) - } - } + coverContent() + content() + BadgeGroup(content = badges) } } + +@Composable +fun RowScope.MangaListItemContent( + text: String, +) { + Text( + text = text, + modifier = Modifier + .padding(horizontal = horizontalPadding) + .weight(1f), + maxLines = 2, + style = MaterialTheme.typography.bodyMedium, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index 6652c5898..271b90f74 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import java.io.File import java.io.IOException import java.io.InputStream +import eu.kanade.domain.manga.model.Manga as DomainManga /** * Class used to create cover cache. @@ -87,6 +88,20 @@ class CoverCache(private val context: Context) { return deleted } + fun deleteFromCache(manga: DomainManga, deleteCustomCover: Boolean = false): Int { + var amountDeleted = 0 + + getCoverFile(manga.thumbnailUrl)?.let { + if (it.exists() && it.delete()) amountDeleted++ + } + + if (deleteCustomCover && deleteCustomCover(manga.id)) { + amountDeleted++ + } + + return amountDeleted + } + /** * Delete custom cover of the manga from the cache * 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 2929350b6..9ad0d7091 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 @@ -1,14 +1,17 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import android.os.Bundle -import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.core.os.bundleOf import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.SourceSearchScreen import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -26,19 +29,30 @@ class SourceSearchController( this.targetController = targetController } - override fun onItemClick(view: View, position: Int): Boolean { - val manga = (adapter?.getItem(position) as? SourceItem)?.manga ?: return false - val migrationListController = targetController as? MigrationListController ?: return false - val sourceManager = Injekt.get() - val source = sourceManager.get(manga.source) ?: return false - migrationListController.useMangaForMigration(manga, source) - router.popCurrentController() - router.popCurrentController() - return true - } + @Composable + override fun ComposeContent() { + SourceSearchScreen( + presenter = presenter, + navigateUp = { router.popCurrentController() }, + onFabClick = { filterSheet?.show() }, + // SY --> + onClickManga = { manga -> + val migrationListController = targetController as? MigrationListController ?: return@SourceSearchScreen + val sourceManager = Injekt.get() + val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen + migrationListController.useMangaForMigration(manga, source) + router.popCurrentController() + router.popCurrentController() + }, + onSettingsClick = { + router.pushController(SourcePreferencesController(presenter.source!!.id)) + }, + // SY <-- + ) - override fun onItemLongClick(position: Int) { - view?.let { super.onItemClick(it, position) } + LaunchedEffect(presenter.filters) { + initFilterSheet() + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt new file mode 100644 index 000000000..85b2bb34b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowsePagingSource.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.ui.browse.source.browse + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.MetadataMangasPage +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.lang.withIOContext +import exh.metadata.metadata.base.RaisedSearchMetadata + +abstract class BrowsePagingSource : PagingSource */ Pair /*SY <-- */>() { + + abstract suspend fun requestNextPage(currentPage: Int): MangasPage + + override suspend fun load(params: LoadParams): LoadResult */ Pair/*SY <-- */> { + val page = params.key ?: 1 + + val mangasPage = try { + withIOContext { + requestNextPage(page.toInt()) + } + } catch (e: Exception) { + return LoadResult.Error(e) + } + // SY --> + val metadata = if (mangasPage is MetadataMangasPage) { + mangasPage.mangasMetadata + } else emptyList() + // SY <-- + + return LoadResult.Page( + data = mangasPage.mangas + // SY --> + .mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) }, + // SY <-- + prevKey = null, + nextKey = if (mangasPage.hasNextPage) page + 1 else null, + ) + } + + override fun getRefreshKey(state: PagingState>): Long? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey ?: anchorPage?.nextKey + } + } +} 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 5f63d7c61..017d6fa55 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,86 +1,35 @@ package eu.kanade.tachiyomi.ui.browse.source.browse -import android.content.res.Configuration import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.core.view.updatePadding -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.fredporciuncula.flow.preferences.Preference +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.snackbar.Snackbar -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.domain.category.model.Category -import eu.kanade.domain.manga.model.Manga -import eu.kanade.domain.manga.model.toDbManga 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 eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.SourceControllerBinding import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.LoginSource -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController +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.ui.browse.source.SourcesController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog +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.more.MoreController -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.lang.withUIContext -import eu.kanade.tachiyomi.util.preference.asHotFlow -import eu.kanade.tachiyomi.util.system.connectivityManager -import eu.kanade.tachiyomi.util.system.getParcelableCompat -import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.shrinkOnScroll -import eu.kanade.tachiyomi.util.view.snack -import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import eu.kanade.tachiyomi.widget.EmptyView -import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput -import exh.log.xLogW import exh.savedsearches.EXHSavedSearch -import exh.source.anyIs -import exh.source.getMainSource -import exh.source.isEhBasedSource -import exh.widget.preference.MangadexLoginDialog -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import uy.kohesive.injekt.injectLazy +import exh.util.nullIfBlank open class BrowseSourceController(bundle: Bundle) : - SearchableNucleusController(bundle), - FabController, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.EndlessScrollListener, - ChangeMangaCategoriesDialog.Listener { + FullComposeController(bundle) { constructor( sourceId: Long, @@ -145,48 +94,75 @@ open class BrowseSourceController(bundle: Bundle) : filterList, ) - private val preferences: PreferencesHelper by injectLazy() - - /** - * Adapter containing the list of manga from the catalogue. - */ - /* SY --> */ - protected /* SY <-- */ var adapter: FlexibleAdapter>? = null - - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null - - /** - * Snackbar containing an error message when a request fails. - */ - private var snack: Snackbar? = null - /** * Sheet containing filter items. */ - private var filterSheet: SourceFilterSheet? = null + protected var filterSheet: SourceFilterSheet? = null - /** - * Recycler view with the list of results. - */ - private var recycler: RecyclerView? = null + @Composable + override fun ComposeContent() { + val scope = rememberCoroutineScope() - /** - * Subscription for the number of manga per row. - */ - private var numColumnsJob: Job? = null + BrowseSourceScreen( + presenter = presenter, + navigateUp = { router.popCurrentController() }, + onDisplayModeChange = { presenter.displayMode = (it) }, + onFabClick = { 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) + } + } + }, + // SY --> + onSettingsClick = { + router.pushController(SourcePreferencesController(presenter.source!!.id)) + }, + // SY <-- + ) - /** - * Endless loading item. - */ - private var progressItem: ProgressItem? = null + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + 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) + }, + ) + } + is Dialog.ChangeMangaCategory -> { + ChangeCategoryDialog( + initialSelection = dialog.initialSelection, + onDismissRequest = onDismissRequest, + onEditCategories = { + router.pushController(CategoryController()) + }, + onConfirm = { include, _ -> + presenter.changeMangaFavorite(dialog.manga) + presenter.moveMangaToCategories(dialog.manga, include) + }, + ) + } + null -> {} + } - init { - setHasOptionsMenu(true) - } - - override fun getTitle(): String? { - return presenter.source.name + LaunchedEffect(presenter.filters) { + initFilterSheet() + } } override fun createPresenter(): BrowseSourcePresenter { @@ -194,61 +170,29 @@ open class BrowseSourceController(bundle: Bundle) : return BrowseSourcePresenter( args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY), - filters = args.getString(FILTERS_CONFIG_KEY), + filtersJson = args.getString(FILTERS_CONFIG_KEY), savedSearch = args.getLong(SAVED_SEARCH_CONFIG_KEY, 0).takeUnless { it == 0L }, ) // SY <-- } - override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this) - setupRecycler(view) - - binding.progress.isVisible = true - - // SY --> - val mainSource = presenter.source.getMainSource() - if (mainSource != null && mainSource.requiresLogin && !mainSource.isLogged()) { - val dialog = MangadexLoginDialog(mainSource) - dialog.showDialog(router) - } - // SY <-- - - presenter.restartPager() - } - fun setSavedSearches(savedSearches: List) { filterSheet?.setSavedSearches(savedSearches) } open fun initFilterSheet() { - if (presenter.sourceFilters.isEmpty()) { - // SY --> - actionFab?.text = activity!!.getString(R.string.saved_searches) - // SY <-- - } - filterSheet = SourceFilterSheet( activity!!, // SY --> this, - presenter.source, + presenter.source!!, emptyList(), // SY <-- onFilterClicked = { - showProgressBar() - adapter?.clear() - presenter.setSourceFilter(presenter.sourceFilters) + presenter.setSourceFilter(presenter.filters) }, onResetClicked = { - presenter.appliedFilters = FilterList() - val newFilters = presenter.source.getFilterList() - presenter.sourceFilters = newFilters + presenter.resetFilter() filterSheet?.setFilters(presenter.filterItems) }, // EXH --> @@ -264,7 +208,7 @@ open class BrowseSourceController(bundle: Bundle) : } .setPositiveButton(R.string.action_save) { _, _ -> if (searchName.isNotBlank() && searchName !in names) { - presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters) + presenter.saveSearch(searchName.trim(), presenter.searchQuery.orEmpty(), presenter.filters) } else { it.toast(R.string.save_search_invalid_name) } @@ -293,14 +237,14 @@ open class BrowseSourceController(bundle: Bundle) : return@launchUI } - presenter.sourceFilters = FilterList(search.filterList) + presenter.setFilter(FilterList(search.filterList)) filterSheet?.setFilters(presenter.filterItems) - val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + val allDefault = presenter.filters == presenter.source!!.getFilterList() - showProgressBar() - adapter?.clear() filterSheet?.dismiss() - presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) + presenter.searchQuery = search.query.nullIfBlank() + presenter.setSourceFilter(if (allDefault) FilterList() else presenter.filters) + presenter.search() activity?.invalidateOptionsMenu() } }, @@ -322,199 +266,16 @@ open class BrowseSourceController(bundle: Bundle) : filterSheet?.setSavedSearches(presenter.loadSearches()) } filterSheet?.setFilters(presenter.filterItems) - - filterSheet?.setOnShowListener { actionFab?.hide() } - filterSheet?.setOnDismissListener { actionFab?.show() } - - actionFab?.setOnClickListener { filterSheet?.show() } - - actionFab?.show() } - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - - fab.setText(R.string.action_filter) - fab.setIconResource(R.drawable.ic_filter_list_24dp) - - // Controlled by initFilterSheet() - fab.hide() - initFilterSheet() - } - - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) } - actionFab = null - } - - override fun onDestroyView(view: View) { - numColumnsJob?.cancel() - numColumnsJob = null - adapter = null - snack = null - recycler = null - super.onDestroyView(view) - } - - private fun setupRecycler(view: View) { - numColumnsJob?.cancel() - - var oldPosition = RecyclerView.NO_POSITION - val oldRecycler = binding.catalogueView.getChildAt(1) - if (oldRecycler is RecyclerView) { - oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() - oldRecycler.adapter = null - - binding.catalogueView.removeView(oldRecycler) - } - - val recycler = if (preferences.sourceDisplayMode().get() == LibraryDisplayMode.List /* SY --> */ || (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) /* SY <-- */) { - RecyclerView(view.context).apply { - id = R.id.recycler - layoutManager = LinearLayoutManager(context) - layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - } - } else { - (binding.catalogueView.inflate(R.layout.source_recycler_autofit) as AutofitRecyclerView).apply { - numColumnsJob = getColumnsPreferenceForCurrentOrientation().asHotFlow { spanCount = it } - .drop(1) - // Set again the adapter to recalculate the covers height - .onEach { adapter = this@BrowseSourceController.adapter } - .launchIn(viewScope) - - (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when (adapter?.getItemViewType(position)) { - R.layout.source_compact_grid_item, R.layout.source_comfortable_grid_item -> 1 - else -> spanCount - } - } - } - } - } - - if (filterSheet != null) { - // Add bottom padding if filter FAB is visible - recycler.updatePadding(bottom = view.resources.getDimensionPixelOffset(R.dimen.fab_list_padding)) - recycler.clipToPadding = false - - actionFab?.shrinkOnScroll(recycler) - } - - recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - binding.catalogueView.addView(recycler, 1) - - if (oldPosition != RecyclerView.NO_POSITION) { - recycler.layoutManager?.scrollToPosition(oldPosition) - } - this.recycler = recycler - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search) - val searchItem = menu.findItem(R.id.action_search) - - searchItem.fixExpand( - onExpand = { invalidateMenuOnExpand() }, - onCollapse = { - if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController) { - router.popController(this) - } else { - nonSubmittedQuery = "" - searchWithQuery("") - } - - true - }, - ) - - val displayItem = when (preferences.sourceDisplayMode().get()) { - LibraryDisplayMode.List -> R.id.action_list - LibraryDisplayMode.ComfortableGrid -> R.id.action_comfortable_grid - else -> R.id.action_compact_grid - } - menu.findItem(displayItem).isChecked = true - // SY --> - if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) { - menu.findItem(R.id.action_display_mode).isVisible = false - } - // SY <-- - } - - override fun onSearchViewQueryTextSubmit(query: String?) { - searchWithQuery(query ?: "") - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - - val isHttpSource = presenter.source is HttpSource - menu.findItem(R.id.action_open_in_web_view)?.isVisible = isHttpSource - - val isLocalSource = presenter.source is LocalSource - menu.findItem(R.id.action_local_source_help)?.isVisible = isLocalSource - - // SY --> - menu.findItem(R.id.action_settings)?.isVisible = presenter.source.anyIs() - // SY <-- - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_search -> expandActionViewFromInteraction = true - R.id.action_compact_grid -> setDisplayMode(LibraryDisplayMode.CompactGrid) - R.id.action_comfortable_grid -> setDisplayMode(LibraryDisplayMode.ComfortableGrid) - R.id.action_list -> setDisplayMode(LibraryDisplayMode.List) - R.id.action_open_in_web_view -> openInWebView() - // SY --> - R.id.action_settings -> openSourceSettings() - // SY <-- - R.id.action_local_source_help -> openLocalSourceHelpGuide() - } - return super.onOptionsItemSelected(item) - } - - private fun openInWebView() { - val source = presenter.source as? HttpSource ?: return - - val activity = activity ?: return - val intent = WebViewActivity.newIntent(activity, source.baseUrl, source.id, presenter.source.name) - startActivity(intent) - } - - private fun openLocalSourceHelpGuide() { - activity?.openInBrowser(LocalSource.HELP_URL) - } - - // SY --> - private fun openSourceSettings() { - router.pushController(SourcePreferencesController(presenter.source.id)) - } - // SY <-- - /** * Restarts the request with a new query. * * @param newQuery the new query. */ fun searchWithQuery(newQuery: String) { - // If text didn't change, do nothing - if (presenter.query == newQuery) { - return - } - - showProgressBar() - adapter?.clear() - - presenter.restartPager(newQuery, presenter.sourceFilters) + presenter.searchQuery = newQuery + presenter.search() } /** @@ -525,7 +286,7 @@ open class BrowseSourceController(bundle: Bundle) : * @param genreName the name of the genre */ fun searchWithGenre(genreName: String) { - val defaultFilters = presenter.source.getFilterList() + val defaultFilters = presenter.source!!.getFilterList() var genreExists = false @@ -555,335 +316,16 @@ open class BrowseSourceController(bundle: Bundle) : } if (genreExists) { - presenter.sourceFilters = defaultFilters filterSheet?.setFilters(presenter.filterItems) - showProgressBar() - - adapter?.clear() - presenter.restartPager("", defaultFilters) + presenter.searchQuery = "" + presenter.setFilter(defaultFilters) } else { searchWithQuery(genreName) } } - /** - * Called from the presenter when the network request is received. - * - * @param page the current page. - * @param mangas the list of manga of the page. - */ - fun onAddPage(page: Int, mangas: List) { - val adapter = adapter ?: return - hideProgressBar() - if (page == 1) { - adapter.clear() - resetProgressItem() - } - adapter.onLoadMoreComplete(mangas) - } - - /** - * Called from the presenter when the network request fails. - * - * @param error the error received. - */ - /* SY --> */ - open /* SY <-- */fun onAddPageError(error: Throwable) { - // SY --> - xLogW("> Failed to load next catalogue page!", error) - xLogW( - "> (source.id: %s, source.name: %s)", - presenter.source.id, - presenter.source.name, - ) - // SY <-- - - val adapter = adapter ?: return - adapter.onLoadMoreComplete(null) - hideProgressBar() - - snack?.dismiss() - - val message = getErrorMessage(error) - val retryAction = View.OnClickListener { - // If not the first page, show bottom progress bar. - if (adapter.mainItemCount > 0 && progressItem != null) { - adapter.addScrollableFooterWithDelay(progressItem!!, 0, true) - } else { - showProgressBar() - } - presenter.requestNext() - } - - if (adapter.isEmpty) { - val actions = if (presenter.source is LocalSource) { - listOf( - EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { openLocalSourceHelpGuide() }, - ) - } else { - listOf( - EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp, retryAction), - EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { openInWebView() }, - EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { activity?.openInBrowser(MoreController.URL_HELP) }, - ) - } - - binding.emptyView.show(message, actions) - } else { - snack = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(message, Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_retry, retryAction) - } - } - } - - private fun getErrorMessage(error: Throwable): String { - if (error is NoResultsException) { - return binding.catalogueView.context.getString(R.string.no_results_found) - } - - return when { - error.message == null -> "" - error.message!!.startsWith("HTTP error") -> "${error.message}: ${binding.catalogueView.context.getString(R.string.http_error_hint)}" - else -> error.message!! - } - } - - /** - * Sets a new progress item and reenables the scroll listener. - */ - private fun resetProgressItem() { - progressItem = ProgressItem() - adapter?.endlessTargetCount = 0 - adapter?.setEndlessScrollListener(this, progressItem!!) - } - - /** - * Called by the adapter when scrolled near the bottom. - */ - override fun onLoadMore(lastPosition: Int, currentPage: Int) { - if (presenter.hasNextPage()) { - presenter.requestNext() - } else { - adapter?.onLoadMoreComplete(null) - adapter?.endlessTargetCount = 1 - } - } - - override fun noMoreLoad(newItemsSize: Int) { - } - - /** - * Called from the presenter when a manga is initialized. - * - * @param manga the manga initialized - */ - fun onMangaInitialized(manga: Manga) { - getHolder(manga)?.setImage(manga) - } - - /** - * Sets the current display mode. - * - * @param mode the mode to change to - */ - private fun setDisplayMode(mode: LibraryDisplayMode) { - val view = view ?: return - val adapter = adapter ?: return - - preferences.sourceDisplayMode().set(mode) - activity?.invalidateOptionsMenu() - setupRecycler(view) - - // Initialize mangas if not on a metered connection - if (!view.context.connectivityManager.isActiveNetworkMetered) { - val mangas = (0 until adapter.itemCount).mapNotNull { - (adapter.getItem(it) as? SourceItem)?.manga - } - presenter.initializeMangas(mangas) - } - } - - /** - * Returns a preference for the number of manga per row based on the current orientation. - * - * @return the preference. - */ - private fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { - preferences.portraitColumns() - } else { - preferences.landscapeColumns() - } - } - - /** - * Returns the view holder for the given manga. - * - * @param manga the manga to find. - * @return the holder of the manga or null if it's not bound. - */ - private fun getHolder(manga: Manga): SourceHolder<*>? { - val adapter = adapter ?: return null - - adapter.allBoundViewHolders.forEach { holder -> - val item = adapter.getItem(holder.bindingAdapterPosition) as? SourceItem - if (item != null && item.manga.id == manga.id) { - return holder as SourceHolder<*> - } - } - - return null - } - - /** - * Shows the progress bar. - */ - private fun showProgressBar() { - binding.emptyView.hide() - binding.progress.isVisible = true - snack?.dismiss() - snack = null - } - - /** - * Hides active progress bars. - */ - private fun hideProgressBar() { - binding.emptyView.hide() - binding.progress.isVisible = false - } - - /** - * Called when a manga is clicked. - * - * @param position the position of the element clicked. - * @return true if the item should be selected, false otherwise. - */ - override fun onItemClick(view: View, position: Int): Boolean { - val item = adapter?.getItem(position) as? SourceItem ?: return false - router.pushController( - MangaController( - item.manga.id, - true, - args.getParcelableCompat(MangaController.SMART_SEARCH_CONFIG_EXTRA), - ), - ) - return false - } - - /** - * Called when a manga is long clicked. - * - * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga - * in, the list consists of the default category plus the user's categories. The default category is preselected on - * new manga, and on already favorited manga the manga's categories are preselected. - * - * @param position the position of the element clicked. - */ - override fun onItemLongClick(position: Int) { - val activity = activity ?: return - val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return - viewScope.launchIO { - val duplicateManga = presenter.getDuplicateLibraryManga(manga) - - withUIContext { - if (manga.favorite) { - MaterialAlertDialogBuilder(activity) - .setTitle(manga.title) - .setItems(arrayOf(activity.getString(R.string.remove_from_library))) { _, which -> - when (which) { - 0 -> { - presenter.changeMangaFavorite(manga.toDbManga()) - adapter?.notifyItemChanged(position) - activity.toast(activity.getString(R.string.manga_removed_library)) - } - } - } - .show() - } else { - if (duplicateManga != null) { - AddDuplicateMangaDialog(this@BrowseSourceController, duplicateManga) { - addToLibrary( - manga, - position, - ) - } - .showDialog(router) - } else { - addToLibrary(manga, position) - } - } - } - } - } - - private fun addToLibrary(newManga: Manga, position: Int) { - val activity = activity ?: return - viewScope.launchIO { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } - - withUIContext { - when { - // Default category set - defaultCategory != null -> { - presenter.moveMangaToCategory(newManga.toDbManga(), defaultCategory) - - presenter.changeMangaFavorite(newManga.toDbManga()) - adapter?.notifyItemChanged(position) - activity.toast(activity.getString(R.string.manga_added_library)) - } - - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - presenter.moveMangaToCategory(newManga.toDbManga(), null) - - presenter.changeMangaFavorite(newManga.toDbManga()) - adapter?.notifyItemChanged(position) - activity.toast(activity.getString(R.string.manga_added_library)) - } - - // Choose a category - else -> { - val ids = presenter.getMangaCategoryIds(newManga) - val preselected = categories.map { - if (it.id in ids) { - QuadStateTextView.State.CHECKED.ordinal - } else { - QuadStateTextView.State.UNCHECKED.ordinal - } - }.toTypedArray() - - ChangeMangaCategoriesDialog(this@BrowseSourceController, listOf(newManga), categories, preselected) - .showDialog(router) - } - } - } - } - } - - /** - * Update manga to use selected categories. - * - * @param mangas The list of manga to move to categories. - * @param categories The list of categories where manga will be placed. - */ - override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { - val manga = mangas.firstOrNull() ?: return - - presenter.changeMangaFavorite(manga.toDbManga()) - presenter.updateMangaCategories(manga.toDbManga(), addCategories) - - val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id } - if (position != null) { - adapter?.notifyItemChanged(position) - } - activity?.toast(activity?.getString(R.string.manga_added_library)) - } - - companion object { + protected companion object { const val SOURCE_ID_KEY = "sourceId" const val SEARCH_QUERY_KEY = "searchQuery" 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/BrowseSourcePresenter.kt index c2fe0df3c..13d916663 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/BrowseSourcePresenter.kt @@ -1,12 +1,31 @@ package eu.kanade.tachiyomi.ui.browse.source.browse +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.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.PagingSource +import androidx.paging.cachedIn +import androidx.paging.map import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.core.prefs.CheckboxState +import eu.kanade.core.prefs.mapAsCheckboxState import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.chapter.interactor.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga +import eu.kanade.domain.manga.interactor.GetFlatMetadataById import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.interactor.InsertManga import eu.kanade.domain.manga.interactor.UpdateManga @@ -17,6 +36,8 @@ import eu.kanade.domain.source.interactor.GetExhSavedSearch import eu.kanade.domain.source.interactor.InsertSavedSearch import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.model.toDomainTrack +import eu.kanade.presentation.browse.BrowseSourceState +import eu.kanade.presentation.browse.BrowseSourceStateImpl import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.toDomainManga @@ -25,10 +46,12 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager 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.model.SManga +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 @@ -47,23 +70,25 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.system.logcat +import exh.metadata.metadata.base.RaisedSearchMetadata import exh.savedsearches.models.SavedSearch +import exh.source.getMainSource import exh.source.isEhBasedSource import exh.util.nullIfBlank -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -80,9 +105,10 @@ open class BrowseSourcePresenter( private val sourceId: Long, searchQuery: String? = null, // SY --> - private val filters: String? = null, + 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(), private val preferences: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), @@ -95,48 +121,91 @@ open class BrowseSourcePresenter( private val updateManga: UpdateManga = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), + // SY --> + private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(), private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(), private val insertSavedSearch: InsertSavedSearch = Injekt.get(), private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(), // SY <-- -) : BasePresenter() { +) : BasePresenter(), BrowseSourceState by state { - /** - * Selected source. - */ - lateinit var source: CatalogueSource + var displayMode by preferences.sourceDisplayMode().asState() - /** - * Modifiable list of filters. - */ - var sourceFilters = FilterList() - set(value) { - field = value - filterItems = value.toItems() + val ehentaiBrowseDisplayMode by preferences.enhancedEHentaiView().asState() + + @Composable + fun getColumnsPreferenceForCurrentOrientation(): State { + val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + return produceState(initialValue = GridCells.Adaptive(128.dp), isLandscape) { + (if (isLandscape) preferences.landscapeColumns() else preferences.portraitColumns()) + .asFlow() + .collectLatest { columns -> + value = if (columns == 0) GridCells.Adaptive(128.dp) else GridCells.Fixed(columns) + } } + } - var filterItems: List> = emptyList() + @Composable + fun getMangaList(): Flow */Pair/* SY <-- */>> { + return remember(currentQuery, appliedFilters) { + Pager( + PagingConfig(pageSize = 25), + ) { + createPager(currentQuery, appliedFilters) + }.flow + .map { + it.map { + // SY --> + withIOContext { + networkToLocalManga(it.first, sourceId).toDomainManga()!! + } to it.second + // SY <-- + } + } + .cachedIn(presenterScope) + } + } - /** - * List of filters used by the [Pager]. If empty alongside [query], the popular query is used. - */ - var appliedFilters = FilterList() + @Composable + fun getManga(initialManga: DomainManga): State { + return produceState(initialValue = initialManga, initialManga.url, initialManga.source) { + getManga.subscribe(initialManga.url, initialManga.source) + .collectLatest { manga -> + if (manga == null) return@collectLatest + launchIO { + initializeMangas(manga) + } + value = manga + } + } + } - /** - * Pager containing a list of manga results. - */ - private lateinit var pager: Pager + @Composable + open fun getRaisedSearchMetadata(manga: DomainManga, 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) + } + } + } - /** - * Subscription for the pager. - */ - private var pagerJob: Job? = null + fun setFilter(filters: FilterList) { + state.filters = filters + } - /** - * Subscription for one request from the pager. - */ - private var nextPageJob: Job? = null + fun resetFilter() { + state.appliedFilters = FilterList() + val newFilters = source!!.getFilterList() + state.filters = newFilters + } + + fun search() { + state.currentQuery = searchQuery ?: "" + } private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } @@ -144,36 +213,32 @@ open class BrowseSourcePresenter( private val filterSerializer = FilterSerializer() // SY <-- - init { - query = searchQuery ?: "" - } - override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - source = sourceManager.get(sourceId) as? CatalogueSource ?: return - sourceFilters = source.getFilterList() + state.source = sourceManager.get(sourceId) as? CatalogueSource ?: return + state.filters = source!!.getFilterList() // SY --> val savedSearchFilters = savedSearch - val jsonFilters = filters + val jsonFilters = filtersJson if (savedSearchFilters != null) { - val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { sourceFilters } } + val savedSearch = runBlocking { getExhSavedSearch.awaitOne(savedSearchFilters) { filters } } if (savedSearch != null) { query = savedSearch.query if (savedSearch.filterList != null) { - appliedFilters = savedSearch.filterList + setFilter(savedSearch.filterList) } } } else if (jsonFilters != null) { runCatching { val filters = Json.decodeFromString(jsonFilters) - filterSerializer.deserialize(sourceFilters, filters) - appliedFilters = sourceFilters + filterSerializer.deserialize(this.filters, filters) + setSourceFilter(this.filters) } } - getExhSavedSearch.subscribe(source.id, source::getFilterList) + getExhSavedSearch.subscribe(source!!.id, source!!::getFilterList) .onEach { withUIContext { view?.setSavedSearches(it) @@ -192,88 +257,6 @@ open class BrowseSourcePresenter( super.onSave(state) } - /** - * Restarts the pager for the active source with the provided query and filters. - * - * @param query the query. - * @param filters the current state of the filters (for search mode). - */ - fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) { - this.query = query - this.appliedFilters = filters - - // Create a new pager. - pager = createPager(query, filters) - - val sourceId = source.id - val sourceDisplayMode = preferences.sourceDisplayMode() - - pagerJob?.cancel() - pagerJob = presenterScope.launchIO { - pager.asFlow() - // SY --> - .map { (first, second, third) -> - Triple( - first, - second.map { - networkToLocalManga( - it, - sourceId, - ).toDomainManga()!! - }, - third, - ) - } - // SY <-- - .onEach { initializeMangas(it.second) } - // SY --> - .map { (first, second, third) -> - first to second.mapIndexed { index, manga -> - SourceItem( - manga, - sourceDisplayMode, - third?.getOrNull(index), - ) - } - } - // SY <-- - .catch { error -> - logcat(LogPriority.ERROR, error) - } - .collectLatest { (page, mangas) -> - withUIContext { - view?.onAddPage(page, mangas) - } - } - } - - // Request first page. - requestNext() - } - - /** - * Requests the next page for the active pager. - */ - fun requestNext() { - if (!hasNextPage()) return - - nextPageJob?.cancel() - nextPageJob = presenterScope.launchIO { - try { - pager.requestNextPage() - } catch (e: Throwable) { - withUIContext { view?.onAddPageError(e) } - } - } - } - - /** - * Returns true if the last fetched page has a next page. - */ - fun hasNextPage(): Boolean { - return pager.hasNextPage - } - /** * Returns a manga from the database for the given manga from network. It creates a new entry * if the manga is not yet in the database. @@ -281,16 +264,14 @@ open class BrowseSourcePresenter( * @param sManga the manga from the source. * @return a manga from the database. */ - private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { - var localManga = runBlocking { getManga.await(sManga.url, sourceId) } + private suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga { + var localManga = getManga.await(sManga.url, sourceId) if (localManga == null) { val newManga = Manga.create(sManga.url, sManga.title, sourceId) newManga.copyFrom(sManga) newManga.id = -1 - val result = runBlocking { - val id = insertManga.await(newManga.toDomainManga()!!) - getManga.await(id!!) - } + val id = insertManga.await(newManga.toDomainManga()!!) + val result = getManga.await(id!!) localManga = result } else if (!localManga.favorite) { // if the manga isn't a favorite, set its display title from source @@ -301,166 +282,131 @@ open class BrowseSourcePresenter( } /** - * Initialize a list of manga. + * Initialize a manga. * * @param mangas the list of manga to initialize. */ - fun initializeMangas(mangas: List) { - presenterScope.launchIO { - mangas.asFlow() - .filter { it.thumbnailUrl == null && !it.initialized } - .map { getMangaDetails(it.toDbManga()) } - .onEach { - withUIContext { - @Suppress("DEPRECATION") - view?.onMangaInitialized(it.toDomainManga()!!) - } - } - .catch { e -> logcat(LogPriority.ERROR, e) } - .collect() + private suspend fun initializeMangas(manga: DomainManga) { + if (manga.thumbnailUrl != null && manga.initialized) return + withContext(NonCancellable) { + val db = manga.toDbManga() + try { + val networkManga = source!!.getMangaDetails(db.copy()) + db.copyFrom(networkManga) + db.initialized = true + updateManga.await( + db + .toDomainManga() + ?.toMangaUpdate()!!, + ) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } } } - /** - * Returns the initialized manga. - * - * @param manga the manga to initialize. - * @return the initialized manga - */ - private suspend fun getMangaDetails(manga: Manga): Manga { - try { - val networkManga = source.getMangaDetails(manga.copy()) - manga.copyFrom(networkManga) - manga.initialized = true - updateManga.await( - manga - .toDomainManga() - ?.toMangaUpdate()!!, - ) - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) - } - return manga - } - /** * Adds or removes a manga from the library. * * @param manga the manga to update. */ - fun changeMangaFavorite(manga: Manga) { - manga.favorite = !manga.favorite - manga.date_added = when (manga.favorite) { - true -> Date().time - false -> 0 - } - - if (!manga.favorite) { - manga.removeCovers(coverCache) - } else { - ChapterSettingsHelper.applySettingDefaults(manga.toDomainManga()!!) - - autoAddTrack(manga) - } - - runBlocking { - updateManga.await( - manga - .toDomainManga() - ?.toMangaUpdate()!!, + fun changeMangaFavorite(manga: DomainManga) { + presenterScope.launch { + var new = manga.copy( + favorite = !manga.favorite, + dateAdded = when (manga.favorite) { + true -> Date().time + false -> 0 + }, ) + + if (!new.favorite) { + new = new.removeCovers(coverCache) + } else { + ChapterSettingsHelper.applySettingDefaults(manga) + + autoAddTrack(manga) + } + + updateManga.await(new.toMangaUpdate()) } } - private fun autoAddTrack(manga: Manga) { - launchIO { - loggedServices - .filterIsInstance() - .filter { it.accept(source) } - .forEach { service -> - try { - service.match(manga)?.let { track -> - track.manga_id = manga.id!! - (service as TrackService).bind(track) - insertTrack.await(track.toDomainTrack()!!) + fun getSourceOrStub(manga: DomainManga): Source { + return sourceManager.getOrStub(manga.source) + } - val chapters = getChapterByMangaId.await(manga.id!!) - syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service) - } - } catch (e: Exception) { - logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } - } + fun addFavorite(manga: DomainManga) { + presenterScope.launch { + val categories = getCategories() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId.toLong() } + + when { + // Default category set + defaultCategory != null -> { + moveMangaToCategories(manga, defaultCategory) + + changeMangaFavorite(manga) + // activity.toast(activity.getString(R.string.manga_added_library)) } + + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + moveMangaToCategories(manga) + + changeMangaFavorite(manga) + // activity.toast(activity.getString(R.string.manga_added_library)) + } + + // Choose a category + else -> { + val preselectedIds = getCategories.await(manga.id).map { it.id } + state.dialog = Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }) + } + } } } + private suspend fun autoAddTrack(manga: DomainManga) { + loggedServices + .filterIsInstance() + .filter { it.accept(source!!) } + .forEach { service -> + try { + service.match(manga.toDbManga())?.let { track -> + track.manga_id = manga.id + (service as TrackService).bind(track) + insertTrack.await(track.toDomainTrack()!!) + + val chapters = getChapterByMangaId.await(manga.id) + syncChaptersWithTrackServiceTwoWay.await(chapters, track.toDomainTrack()!!, service) + } + } catch (e: Exception) { + logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } + } + } + } + /** * Set the filter states for the current source. * * @param filters a list of active filters. */ fun setSourceFilter(filters: FilterList) { - restartPager(filters = filters) + state.appliedFilters = filters } - open fun createPager(query: String, filters: FilterList): Pager { + open fun createPager(query: String, filters: FilterList): PagingSource> { // SY --> - return if (source.isEhBasedSource()) { - EHentaiPager(source, query, filters) + return if (source!!.isEhBasedSource()) { + EHentaiBrowsePagingSource(source!!, query, filters) } else { - SourcePager(source, query, filters) + SourceBrowsePagingSource(source!!, query, filters) } // SY <-- } - // SY --> - companion object { - // SY <-- - fun FilterList.toItems(): List> { - return mapNotNull { filter -> - when (filter) { - is Filter.Header -> HeaderItem(filter) - // --> EXH - is Filter.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<*> -> { - 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) - // SY --> - is Filter.AutoComplete -> AutoCompleteSectionItem(it) - // SY <-- - else -> null - } - } - subItems.forEach { it.header = group } - group.subItems = subItems - group - } - is Filter.Sort -> { - val group = SortGroup(filter) - val subItems = filter.values.map { - SortItem(it, group) - } - group.subItems = subItems - group - } - } - } - } - // SY --> - } - // SY <-- - /** * Get user categories. * @@ -477,55 +423,32 @@ open class BrowseSourcePresenter( return getDuplicateLibraryManga.await(manga.title, manga.source) } - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: DomainManga): Array { - return runBlocking { getCategories.await(manga.id) } - .map { it.id } - .toTypedArray() - } - /** * Move the given manga to categories. * * @param categories the selected categories. * @param manga the manga to move. */ - private fun moveMangaToCategories(manga: Manga, categories: List) { + fun moveMangaToCategories(manga: DomainManga, vararg categories: DomainCategory) { + moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id }) + } + + fun moveMangaToCategories(manga: DomainManga, categoryIds: List) { presenterScope.launchIO { setMangaCategories.await( - mangaId = manga.id!!, - categoryIds = categories.filter { it.id != 0L }.map { it.id }, + mangaId = manga.id, + categoryIds = categoryIds.toList(), ) } } - /** - * Move the given manga to the category. - * - * @param category the selected category. - * @param manga the manga to move. - */ - fun moveMangaToCategory(manga: Manga, category: DomainCategory?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - - /** - * Update manga to use selected categories. - * - * @param manga needed to change - * @param selectedCategories selected categories - */ - fun updateMangaCategories(manga: Manga, selectedCategories: List) { - if (!manga.favorite) { - changeMangaFavorite(manga) - } - - moveMangaToCategories(manga, selectedCategories) + sealed class Dialog { + data class RemoveManga(val manga: DomainManga) : Dialog() + data class AddDuplicateManga(val manga: DomainManga, val duplicate: DomainManga) : Dialog() + data class ChangeMangaCategory( + val manga: DomainManga, + val initialSelection: List>, + ) : Dialog() } // EXH --> @@ -534,7 +457,7 @@ open class BrowseSourcePresenter( insertSavedSearch.await( SavedSearch( id = -1, - source = source.id, + source = source!!.id, name = name.trim(), query = query.nullIfBlank(), filtersJson = runCatching { filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) } }.getOrNull(), @@ -550,9 +473,51 @@ open class BrowseSourcePresenter( } 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) + // <-- 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<*> -> { + 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) + // SY --> + is Filter.AutoComplete -> AutoCompleteSectionItem(it) + // SY <-- + else -> null + } + } + subItems.forEach { it.header = group } + group.subItems = subItems + group + } + is Filter.Sort -> { + val group = SortGroup(filter) + val subItems = filter.values.map { + SortItem(it, group) + } + group.subItems = subItems + group + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiBrowsePagingSource.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiBrowsePagingSource.kt new file mode 100644 index 000000000..dad23f2d8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiBrowsePagingSource.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.ui.browse.source.browse + +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.MetadataMangasPage +import eu.kanade.tachiyomi.util.lang.awaitSingle + +class EHentaiBrowsePagingSource(val source: CatalogueSource, val query: String, val filters: FilterList) : BrowsePagingSource() { + + private var lastMangaLink: String? = null + + override suspend fun requestNextPage(currentPage: Int): MangasPage { + val lastMangaLink = lastMangaLink + + val observable = if (query.isBlank() && filters.isEmpty()) { + source.fetchPopularManga(currentPage) + } else { + source.fetchSearchManga(currentPage, query, filters) + } + + val mangasPage = observable.awaitSingle() + + mangasPage.mangas.lastOrNull()?.let { + this.lastMangaLink = it.url + } + return if (lastMangaLink != null) { + val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink } + if (index != -1) { + val lastIndex = mangasPage.mangas.size + val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex) + if (mangasPage is MetadataMangasPage) { + mangasPage.copy( + mangas = mangasPage.mangas.subList(startIndex, lastIndex), + mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex), + ) + } else { + mangasPage.copy( + mangas = mangasPage.mangas.subList(startIndex, lastIndex), + ) + } + } else { + mangasPage + } + } else { + mangasPage + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiPager.kt deleted file mode 100644 index fc63d4a84..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/EHentaiPager.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MetadataMangasPage -import eu.kanade.tachiyomi.util.lang.awaitSingle - -class EHentaiPager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() { - - private var lastMangaLink: String? = null - - override suspend fun requestNextPage() { - val page = currentPage - val lastMangaLink = lastMangaLink - - val observable = if (query.isBlank() && filters.isEmpty()) { - source.fetchPopularManga(page) - } else { - source.fetchSearchManga(page, query, filters) - } - - val mangasPage = observable.awaitSingle() - - mangasPage.mangas.lastOrNull()?.let { - this.lastMangaLink = it.url - } - if (lastMangaLink != null) { - val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink } - if (index != -1) { - val lastIndex = mangasPage.mangas.size - val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex) - onPageReceived( - if (mangasPage is MetadataMangasPage) { - mangasPage.copy( - mangas = mangasPage.mangas.subList(startIndex, lastIndex), - mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex), - ) - } else { - mangasPage.copy( - mangas = mangasPage.mangas.subList(startIndex, lastIndex), - ) - }, - ) - } else { - onPageReceived(mangasPage) - } - } else { - onPageReceived(mangasPage) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt deleted file mode 100644 index da628574d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.core.util.asFlow -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.MetadataMangasPage -import eu.kanade.tachiyomi.source.model.SManga -import exh.metadata.metadata.base.RaisedSearchMetadata -import kotlinx.coroutines.flow.Flow - -/** - * A general pager for source requests (latest updates, popular, search) - */ -abstract class Pager(var currentPage: Int = 1) { - - var hasNextPage = true - private set - - protected val results: PublishRelay */ Triple /* SY <-- */ /* SY --> */, List? /* SY <-- */>> = PublishRelay.create() - - fun asFlow(): Flow */ Triple /* SY <-- */ /* SY --> */, List?> /* SY <-- */> { - return results.asObservable().asFlow() - } - - abstract suspend fun requestNextPage() - - fun onPageReceived(mangasPage: MangasPage) { - val page = currentPage - currentPage++ - hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty() - // SY --> - val mangasMetadata = if (mangasPage is MetadataMangasPage) { - mangasPage.mangasMetadata - } else null - // SY <-- - results.call( /* SY <-- */ Triple /* SY <-- */ (page, mangasPage.mangas /* SY --> */, mangasMetadata /* SY <-- */)) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/ProgressItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/ProgressItem.kt deleted file mode 100644 index df46f54a0..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/ProgressItem.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import android.view.View -import android.widget.ProgressBar -import android.widget.TextView -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R - -class ProgressItem : AbstractFlexibleItem() { - - private var loadMore = true - - override fun getLayoutRes(): Int { - return R.layout.source_progress_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List) { - holder.progressBar.isVisible = false - holder.progressMessage.isVisible = false - - if (!adapter.isEndlessScrollEnabled) { - loadMore = false - } - - if (loadMore) { - holder.progressBar.isVisible = true - } else { - holder.progressMessage.isVisible = true - } - } - - override fun equals(other: Any?): Boolean { - return this === other - } - - override fun hashCode(): Int { - return loadMore.hashCode() - } - - class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { - - val progressBar: ProgressBar = view.findViewById(R.id.progress_bar) - val progressMessage: TextView = view.findViewById(R.id.progress_message) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt new file mode 100644 index 000000000..5bb308724 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceBrowsePagingSource.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.ui.browse.source.browse + +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.util.lang.awaitSingle + +class SourceBrowsePagingSource(val source: CatalogueSource, val query: String, val filters: FilterList) : BrowsePagingSource() { + + override suspend fun requestNextPage(currentPage: Int): MangasPage { + val observable = if (query.isBlank() && filters.isEmpty()) { + source.fetchPopularManga(currentPage) + } else { + source.fetchSearchManga(currentPage, query, filters) + } + + return observable.awaitSingle() + .takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt deleted file mode 100644 index ccbcf92f5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import androidx.core.view.isVisible -import coil.dispose -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher -import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding -import eu.kanade.tachiyomi.util.view.loadAutoPause -import exh.metadata.metadata.MangaDexSearchMetadata -import exh.metadata.metadata.base.RaisedSearchMetadata - -/** - * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. - * All the elements from the layout file "item_source_grid" are available in this class. - * - * @param binding the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new catalogue holder. - */ -class SourceComfortableGridHolder( - override val binding: SourceComfortableGridItemBinding, - adapter: FlexibleAdapter<*>, -) : SourceHolder(binding.root, adapter) { - - /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - // Set manga title - binding.title.text = manga.title - - // Set alpha of thumbnail. - binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f - - // For rounded corners - binding.badges.leftBadges.clipToOutline = true - binding.badges.rightBadges.clipToOutline = true - - // Set favorite badge - binding.badges.favoriteText.isVisible = manga.favorite - - setImage(manga) - } - - // SY --> - override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { - if (metadata is MangaDexSearchMetadata) { - metadata.followStatus?.let { - binding.badges.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] - binding.badges.localText.isVisible = true - } - metadata.relation?.let { - binding.badges.localText.setText(it.resId) - binding.badges.localText.isVisible = true - } - } - } - // SY <-- - - override fun setImage(manga: Manga) { - binding.thumbnail.dispose() - binding.thumbnail.loadAutoPause(manga) { - setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt deleted file mode 100644 index fdc201e6a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import androidx.core.view.isVisible -import coil.dispose -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher -import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding -import eu.kanade.tachiyomi.util.view.loadAutoPause -import exh.metadata.metadata.MangaDexSearchMetadata -import exh.metadata.metadata.base.RaisedSearchMetadata - -/** - * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. - * All the elements from the layout file "item_source_grid" are available in this class. - * - * @param binding the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new catalogue holder. - */ -class SourceCompactGridHolder( - override val binding: SourceCompactGridItemBinding, - adapter: FlexibleAdapter<*>, -) : SourceHolder(binding.root, adapter) { - - /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - // Set manga title - binding.title.text = manga.title - - // Set alpha of thumbnail. - binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f - - // For rounded corners - binding.badges.leftBadges.clipToOutline = true - binding.badges.rightBadges.clipToOutline = true - - // Set favorite badge - binding.badges.favoriteText.isVisible = manga.favorite - - setImage(manga) - } - - // SY --> - override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { - if (metadata is MangaDexSearchMetadata) { - metadata.followStatus?.let { - binding.badges.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] - binding.badges.localText.isVisible = true - } - metadata.relation?.let { - binding.badges.localText.setText(it.resId) - binding.badges.localText.isVisible = true - } - } - } - // SY <-- - - override fun setImage(manga: Manga) { - binding.thumbnail.dispose() - binding.thumbnail.loadAutoPause(manga) { - setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt deleted file mode 100644 index e03940adb..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt +++ /dev/null @@ -1,108 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import android.view.View -import androidx.core.view.isVisible -import coil.dispose -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher -import eu.kanade.tachiyomi.databinding.SourceEnhancedEhentaiListItemBinding -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.view.loadAutoPause -import exh.metadata.MetadataUtil -import exh.metadata.metadata.EHentaiSearchMetadata -import exh.metadata.metadata.base.RaisedSearchMetadata -import exh.util.SourceTagsUtil -import java.util.Date - -/** - * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. - * All the elements from the layout file "item_catalogue_list" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new catalogue holder. - */ -class SourceEnhancedEHentaiListHolder(view: View, adapter: FlexibleAdapter<*>) : - SourceHolder(view, adapter) { - - override val binding = SourceEnhancedEhentaiListItemBinding.bind(itemView) - - private val favoriteColor = itemView.context.getResourceColor(R.attr.colorOnSurface, 0.38f) - private val unfavoriteColor = itemView.context.getResourceColor(R.attr.colorOnSurface) - - /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - binding.title.text = manga.title - binding.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor) - - // Set alpha of thumbnail. - binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f - - // For rounded corners - binding.badges.leftBadges.clipToOutline = true - binding.badges.rightBadges.clipToOutline = true - - // Set favorite badge - binding.badges.favoriteText.isVisible = manga.favorite - - setImage(manga) - } - - override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { - if (metadata !is EHentaiSearchMetadata) return - - if (metadata.uploader != null) { - binding.uploader.text = metadata.uploader - } - - val pair = when (metadata.genre) { - "doujinshi" -> SourceTagsUtil.GenreColor.DOUJINSHI_COLOR to R.string.doujinshi - "manga" -> SourceTagsUtil.GenreColor.MANGA_COLOR to R.string.manga - "artistcg" -> SourceTagsUtil.GenreColor.ARTIST_CG_COLOR to R.string.artist_cg - "gamecg" -> SourceTagsUtil.GenreColor.GAME_CG_COLOR to R.string.game_cg - "western" -> SourceTagsUtil.GenreColor.WESTERN_COLOR to R.string.western - "non-h" -> SourceTagsUtil.GenreColor.NON_H_COLOR to R.string.non_h - "imageset" -> SourceTagsUtil.GenreColor.IMAGE_SET_COLOR to R.string.image_set - "cosplay" -> SourceTagsUtil.GenreColor.COSPLAY_COLOR to R.string.cosplay - "asianporn" -> SourceTagsUtil.GenreColor.ASIAN_PORN_COLOR to R.string.asian_porn - "misc" -> SourceTagsUtil.GenreColor.MISC_COLOR to R.string.misc - else -> null - } - - if (pair != null) { - binding.genre.setBackgroundColor(pair.first.color) - binding.genre.text = itemView.context.getString(pair.second) - } else binding.genre.text = metadata.genre - - metadata.datePosted?.let { binding.datePosted.text = MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } - - metadata.averageRating?.let { binding.ratingBar.rating = it.toFloat() } - - val locale = SourceTagsUtil.getLocaleSourceUtil( - metadata.tags - .firstOrNull { it.namespace == EHentaiSearchMetadata.EH_LANGUAGE_NAMESPACE } - ?.name, - ) - val pageCount = metadata.length - - binding.language.text = if (locale != null && pageCount != null) { - itemView.resources.getQuantityString(R.plurals.browse_language_and_pages, pageCount, pageCount, locale.toLanguageTag().uppercase()) - } else if (pageCount != null) { - itemView.resources.getQuantityString(R.plurals.num_pages, pageCount, pageCount) - } else locale?.toLanguageTag()?.uppercase() - } - - override fun setImage(manga: Manga) { - binding.thumbnail.dispose() - binding.thumbnail.loadAutoPause(manga) { - setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt deleted file mode 100644 index ee7465b36..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import android.view.View -import androidx.viewbinding.ViewBinding -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.domain.manga.model.Manga -import exh.metadata.metadata.base.RaisedSearchMetadata - -/** - * Generic class used to hold the displayed data of a manga in the catalogue. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - */ -abstract class SourceHolder(view: View, adapter: FlexibleAdapter<*>) : - FlexibleViewHolder(view, adapter) { - - abstract val binding: VB - - /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - abstract fun onSetValues(manga: Manga) - - /** - * Updates the image for this holder. Useful to update the image when the manga is initialized - * and the url is now known. - * - * @param manga the manga to bind. - */ - abstract fun setImage(manga: Manga) - - // SY --> - abstract fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt deleted file mode 100644 index 1c769bfcd..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt +++ /dev/null @@ -1,85 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.fredporciuncula.flow.preferences.Preference -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding -import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding -import eu.kanade.tachiyomi.ui.library.setting.LibraryDisplayMode -import exh.metadata.metadata.base.RaisedSearchMetadata -import exh.source.isEhBasedManga -import uy.kohesive.injekt.injectLazy - -class SourceItem(val manga: Manga, private val displayMode: Preference /* SY --> */, private val metadata: RaisedSearchMetadata? = null /* SY <-- */) : - AbstractFlexibleItem>() { - // SY --> - val preferences: PreferencesHelper by injectLazy() - // SY <-- - - override fun getLayoutRes(): Int { - // SY --> - if (manga.isEhBasedManga() && preferences.enhancedEHentaiView().get()) { - return R.layout.source_enhanced_ehentai_list_item - } - // SY <-- - return when (displayMode.get()) { - LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> R.layout.source_compact_grid_item - LibraryDisplayMode.ComfortableGrid -> R.layout.source_comfortable_grid_item - LibraryDisplayMode.List -> R.layout.source_list_item - } - } - - override fun createViewHolder( - view: View, - adapter: FlexibleAdapter>, - ): SourceHolder<*> { - // SY --> - if (manga.isEhBasedManga() && preferences.enhancedEHentaiView().get()) { - return SourceEnhancedEHentaiListHolder(view, adapter) - } - // SY <-- - return when (displayMode.get()) { - LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { - SourceCompactGridHolder(SourceCompactGridItemBinding.bind(view), adapter) - } - LibraryDisplayMode.ComfortableGrid -> { - SourceComfortableGridHolder(SourceComfortableGridItemBinding.bind(view), adapter) - } - LibraryDisplayMode.List -> { - SourceListHolder(view, adapter) - } - } - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: SourceHolder<*>, - position: Int, - payloads: List?, - ) { - holder.onSetValues(manga) - // SY --> - if (metadata != null) { - holder.onSetMetadataValues(manga, metadata) - } - // SY <-- - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is SourceItem) { - return manga.id == other.manga.id - } - return false - } - - override fun hashCode(): Int { - return manga.id.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt deleted file mode 100644 index 7a82810f9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ /dev/null @@ -1,77 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import android.view.View -import androidx.core.view.isVisible -import coil.dispose -import coil.load -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher -import eu.kanade.tachiyomi.databinding.SourceListItemBinding -import eu.kanade.tachiyomi.util.system.getResourceColor -import exh.metadata.metadata.MangaDexSearchMetadata -import exh.metadata.metadata.base.RaisedSearchMetadata - -/** - * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. - * All the elements from the layout file "item_catalogue_list" are available in this class. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new catalogue holder. - */ -class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) : - SourceHolder(view, adapter) { - - override val binding = SourceListItemBinding.bind(view) - - private val favoriteColor = view.context.getResourceColor(R.attr.colorOnSurface, 0.38f) - private val unfavoriteColor = view.context.getResourceColor(R.attr.colorOnSurface) - - /** - * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this - * holder with the given manga. - * - * @param manga the manga to bind. - */ - override fun onSetValues(manga: Manga) { - binding.title.text = manga.title - binding.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor) - - // Set alpha of thumbnail. - binding.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f - - // For rounded corners - binding.badges.clipToOutline = true - - // Set favorite badge - binding.favoriteText.isVisible = manga.favorite - - setImage(manga) - } - - // SY --> - override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { - if (metadata is MangaDexSearchMetadata) { - metadata.followStatus?.let { - binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] - binding.localText.isVisible = true - } - metadata.relation?.let { - binding.localText.setText(it.resId) - binding.localText.isVisible = true - } - } - } - // SY <-- - - override fun setImage(manga: Manga) { - binding.thumbnail.dispose() - if (!manga.thumbnailUrl.isNullOrEmpty()) { - binding.thumbnail.load(manga) { - setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourcePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourcePager.kt deleted file mode 100644 index 786ffca3a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourcePager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.browse - -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.util.lang.awaitSingle - -class SourcePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() { - - override suspend fun requestNextPage() { - val page = currentPage - - val observable = if (query.isBlank() && filters.isEmpty()) { - source.fetchPopularManga(page) - } else { - source.fetchSearchManga(page, query, filters) - } - - val mangasPage = observable.awaitSingle() - - if (mangasPage.mangas.isNotEmpty()) { - onPageReceived(mangasPage) - } else { - throw NoResultsException() - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt index dc22a3a9f..ddacf989c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt @@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems +import eu.kanade.tachiyomi.ui.browse.source.browse.toItems import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.system.logcat diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt new file mode 100644 index 000000000..166a4a184 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesBrowsePagingSource.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.ui.browse.source.latest + +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource +import eu.kanade.tachiyomi.util.lang.awaitSingle + +class LatestUpdatesBrowsePagingSource(val source: CatalogueSource) : BrowsePagingSource() { + + override suspend fun requestNextPage(currentPage: Int): MangasPage { + return source.fetchLatestUpdates(currentPage).awaitSingle() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt index 602e52585..a93f6ba46 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt @@ -1,13 +1,22 @@ package eu.kanade.tachiyomi.ui.browse.source.latest import android.os.Bundle -import android.view.Menu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.core.os.bundleOf import eu.kanade.domain.source.model.Source -import eu.kanade.tachiyomi.R +import eu.kanade.presentation.browse.BrowseLatestScreen +import eu.kanade.presentation.browse.components.RemoveMangaDialog +import eu.kanade.presentation.components.ChangeCategoryDialog +import eu.kanade.presentation.components.DuplicateMangaDialog import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController 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]. @@ -28,9 +37,68 @@ class LatestUpdatesController(bundle: Bundle) : BrowseSourceController(bundle) { return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_search).isVisible = false + @Composable + override fun ComposeContent() { + val scope = rememberCoroutineScope() + + BrowseLatestScreen( + presenter = presenter, + navigateUp = { router.popCurrentController() }, + 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) + } + } + }, + // SY --> + onSettingsClick = { + router.pushController(SourcePreferencesController(presenter.source!!.id)) + }, + // SY <-- + ) + + val onDismissRequest = { presenter.dialog = null } + when (val dialog = presenter.dialog) { + is BrowseSourcePresenter.Dialog.AddDuplicateManga -> { + DuplicateMangaDialog( + onDismissRequest = onDismissRequest, + onOpenManga = { + router.pushController(MangaController(dialog.duplicate.id, true)) + }, + onConfirm = { + presenter.addFavorite(dialog.manga) + }, + duplicateFrom = presenter.getSourceOrStub(dialog.manga), + ) + } + is BrowseSourcePresenter.Dialog.RemoveManga -> { + RemoveMangaDialog( + onDismissRequest = onDismissRequest, + onConfirm = { + presenter.changeMangaFavorite(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) + }, + ) + } + null -> {} + } } override fun initFilterSheet() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPager.kt deleted file mode 100644 index fa07a3462..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPager.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.latest - -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager -import eu.kanade.tachiyomi.util.lang.awaitSingle - -class LatestUpdatesPager(val source: CatalogueSource) : Pager() { - - override suspend fun requestNextPage() { - val mangasPage = source.fetchLatestUpdates(currentPage).awaitSingle() - onPageReceived(mangasPage) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt index 968d3635d..d2a9030e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesPresenter.kt @@ -1,12 +1,14 @@ package eu.kanade.tachiyomi.ui.browse.source.latest +import androidx.paging.PagingSource import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager +import exh.metadata.metadata.base.RaisedSearchMetadata class LatestUpdatesPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) { - override fun createPager(query: String, filters: FilterList): Pager { - return LatestUpdatesPager(source) + override fun createPager(query: String, filters: FilterList): PagingSource */ Pair /*SY <-- */> { + return LatestUpdatesBrowsePagingSource(source!!) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt deleted file mode 100644 index 682f6643b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt +++ /dev/null @@ -1,81 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.domain.category.model.Category -import eu.kanade.domain.manga.model.Manga -import eu.kanade.presentation.category.visualName -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.category.CategoryController -import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView -import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems - -class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { - - private var mangas = emptyList() - private var categories = emptyList() - private var preselected = emptyArray() - private var selected = emptyArray().toIntArray() - - constructor( - target: T, - mangas: List, - categories: List, - preselected: Array, - ) : this() { - this.mangas = mangas - this.categories = categories - this.preselected = preselected - this.selected = preselected.toIntArray() - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.action_move_category) - .setNegativeButton(android.R.string.cancel, null) - .apply { - if (categories.isNotEmpty()) { - setQuadStateMultiChoiceItems( - items = categories.map { it.visualName(context) }, - isActionList = false, - initialSelected = preselected.toIntArray(), - ) { selections -> - selected = selections - } - setPositiveButton(android.R.string.ok) { _, _ -> - val add = selected - .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) categories[index] else null } - .filterNotNull() - val remove = selected - .mapIndexed { index, value -> if (value == QuadStateTextView.State.UNCHECKED.ordinal) categories[index] else null } - .filterNotNull() - (targetController as? Listener)?.updateCategoriesForMangas(mangas, add, remove) - } - setNeutralButton(R.string.action_edit) { _, _ -> openCategoryController() } - } else { - setMessage(R.string.information_empty_category_dialog) - setPositiveButton(R.string.action_edit_categories) { _, _ -> openCategoryController() } - } - } - .create() - } - - private fun openCategoryController() { - if (targetController is LibraryController) { - val libController = targetController as LibraryController - libController.clearSelection() - } - router.popCurrentController() - router.pushController(CategoryController()) - } - - interface Listener { - fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List = emptyList()) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt deleted file mode 100644 index e3d4ec0bd..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt +++ /dev/null @@ -1,48 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.pushController -import uy.kohesive.injekt.injectLazy - -class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) { - - private val sourceManager: SourceManager by injectLazy() - - private lateinit var libraryManga: Manga - private lateinit var onAddToLibrary: () -> Unit - - constructor( - target: Controller, - libraryManga: Manga, - onAddToLibrary: () -> Unit, - ) : this() { - targetController = target - - this.libraryManga = libraryManga - this.onAddToLibrary = onAddToLibrary - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val source = sourceManager.getOrStub(libraryManga.source) - - return MaterialAlertDialogBuilder(activity!!) - .setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name)) - .setPositiveButton(activity?.getString(R.string.action_add)) { _, _ -> - onAddToLibrary() - } - .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ -> - dismissDialog() - router.pushController(MangaController(libraryManga.id)) - } - .setCancelable(true) - .create() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 13f844d22..6c77f2826 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -23,12 +23,12 @@ import eu.kanade.data.chapter.NoChaptersException import eu.kanade.domain.manga.model.toDbManga import eu.kanade.presentation.components.ChangeCategoryDialog import eu.kanade.presentation.components.ChapterDownloadAction +import eu.kanade.presentation.components.DuplicateMangaDialog import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.MangaScreen import eu.kanade.presentation.manga.components.DeleteChaptersDialog import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog -import eu.kanade.presentation.manga.components.DuplicateMangaDialog import eu.kanade.presentation.util.calculateWindowWidthSizeClass import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadService diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 03a2b202c..4fdb2a34d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -51,6 +51,12 @@ fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int { return coverCache.deleteFromCache(this, true) } +fun DomainManga.removeCovers(coverCache: CoverCache = Injekt.get()): DomainManga { + if (isLocal()) return this + coverCache.deleteFromCache(this, true) + return copy(coverLastModified = Date().time) +} + fun DomainManga.shouldDownloadNewChapters(dbCategories: List, preferences: PreferencesHelper): Boolean { if (!favorite) return false diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt index e46049cf7..5c55a2181 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt @@ -1,12 +1,20 @@ package exh.md.follows import android.os.Bundle -import android.view.Menu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.core.os.bundleOf -import eu.kanade.tachiyomi.R +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 eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.base.controller.pushController 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]. @@ -19,22 +27,69 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) ), ) - override fun getTitle(): String? { - return view?.context?.getString(R.string.mangadex_follows) - } - override fun createPresenter(): BrowseSourcePresenter { return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY)) } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_search).isVisible = false - menu.findItem(R.id.action_open_in_web_view).isVisible = false - menu.findItem(R.id.action_settings).isVisible = false + @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) + }, + ) + } + 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) + }, + ) + } + null -> {} + } } override fun initFilterSheet() { - // No-op: we don't allow filtering in latest + // No-op: we don't allow filtering in mangadex follows } } diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt deleted file mode 100644 index 84794a12b..000000000 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt +++ /dev/null @@ -1,14 +0,0 @@ -package exh.md.follows - -import eu.kanade.tachiyomi.source.online.all.MangaDex -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager - -/** - * LatestUpdatesPager inherited from the general Pager. - */ -class MangaDexFollowsPager(val source: MangaDex) : Pager() { - - override suspend fun requestNextPage() { - onPageReceived(source.fetchFollows(currentPage)) - } -} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPagingSource.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPagingSource.kt new file mode 100644 index 000000000..fd4c258c0 --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsPagingSource.kt @@ -0,0 +1,15 @@ +package exh.md.follows + +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.online.all.MangaDex +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource + +/** + * LatestUpdatesPager inherited from the general Pager. + */ +class MangaDexFollowsPagingSource(val source: MangaDex) : BrowsePagingSource() { + + override suspend fun requestNextPage(currentPage: Int): MangasPage { + return source.fetchFollows(currentPage) + } +} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt index 367113d2b..14f6c488d 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt @@ -1,9 +1,16 @@ 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 androidx.paging.PagingSource +import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager +import exh.metadata.metadata.base.RaisedSearchMetadata import exh.source.getMainSource /** @@ -11,8 +18,12 @@ import exh.source.getMainSource */ class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) { - override fun createPager(query: String, filters: FilterList): Pager { - val sourceAsMangaDex = source.getMainSource() as MangaDex - return MangaDexFollowsPager(sourceAsMangaDex) + override fun createPager(query: String, filters: FilterList): PagingSource> { + 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/similar/MangaDexSimilarController.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt index 852702080..bba073ca2 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarController.kt @@ -1,13 +1,17 @@ package exh.md.similar import android.os.Bundle -import android.view.Menu +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.core.os.bundleOf 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.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]. @@ -22,36 +26,28 @@ class MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle) ), ) - private val mangaTitle = args.getString(MANGA_TITLE) - - override fun getTitle(): String? { - return view?.context?.getString(R.string.similar, mangaTitle) - } + private val mangaTitle = args.getString(MANGA_TITLE, "") override fun createPresenter(): BrowseSourcePresenter { return MangaDexSimilarPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY)) } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_search).isVisible = false - menu.findItem(R.id.action_open_in_web_view).isVisible = false - menu.findItem(R.id.action_settings).isVisible = false + @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 } - override fun onItemLongClick(position: Int) { - return - } - - override fun onAddPageError(error: Throwable) { - super.onAddPageError(error) - binding.emptyView.show(activity!!.getString(R.string.similar_no_results)) - } - companion object { const val MANGA_ID = "manga_id" const val MANGA_TITLE = "manga_title" diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarPagingSource.kt similarity index 65% rename from app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt rename to app/src/main/java/exh/md/similar/MangaDexSimilarPagingSource.kt index ce8c41af7..af0c64e24 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarPager.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarPagingSource.kt @@ -1,19 +1,20 @@ package exh.md.similar import eu.kanade.domain.manga.model.Manga +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.online.all.MangaDex +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope /** - * MangaDexSimilarPager inherited from the general Pager. + * MangaDexSimilarPagingSource inherited from the general Pager. */ -class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() { +class MangaDexSimilarPagingSource(val manga: Manga, val source: MangaDex) : BrowsePagingSource() { - override suspend fun requestNextPage() { + override suspend fun requestNextPage(currentPage: Int): MangasPage { val mangasPage = coroutineScope { val similarPageDef = async { source.getMangaSimilar(manga.toSManga()) } val relatedPageDef = async { source.getMangaRelated(manga.toSManga()) } @@ -27,10 +28,6 @@ class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() { ) } - if (mangasPage.mangas.isNotEmpty()) { - onPageReceived(mangasPage) - } else { - throw NoResultsException() - } + return mangasPage.takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException() } } diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt index 0196a7037..a88a96382 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarPresenter.kt @@ -1,11 +1,18 @@ 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 androidx.paging.PagingSource import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager +import exh.metadata.metadata.base.RaisedSearchMetadata import exh.source.getMainSource import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.Injekt @@ -22,9 +29,17 @@ class MangaDexSimilarPresenter( var manga: Manga? = null - override fun createPager(query: String, filters: FilterList): Pager { - val sourceAsMangaDex = source.getMainSource() as MangaDex + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) this.manga = runBlocking { getManga.await(mangaId) } - return MangaDexSimilarPager(manga!!, sourceAsMangaDex) + } + + override fun createPager(query: String, filters: FilterList): PagingSource> { + 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/recs/RecommendsController.kt b/app/src/main/java/exh/recs/RecommendsController.kt index ee8c49761..3a3c38169 100644 --- a/app/src/main/java/exh/recs/RecommendsController.kt +++ b/app/src/main/java/exh/recs/RecommendsController.kt @@ -1,16 +1,14 @@ package exh.recs import android.os.Bundle -import android.view.Menu -import android.view.View +import androidx.compose.runtime.Composable import androidx.core.os.bundleOf import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.R +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.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem /** * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController]. @@ -24,31 +22,26 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) { ), ) - override fun getTitle(): String? { - return (presenter as? RecommendsPresenter)?.manga?.title - } - override fun createPresenter(): RecommendsPresenter { return RecommendsPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY)) } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_search).isVisible = false - menu.findItem(R.id.action_open_in_web_view).isVisible = false - menu.findItem(R.id.action_settings).isVisible = false + @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 } - override fun onItemClick(view: View, position: Int): Boolean { - val item = adapter?.getItem(position) as? SourceItem ?: return false - openSmartSearch(item.manga.ogTitle) - return true - } - private fun openSmartSearch(title: String) { val smartSearchConfig = SourcesController.SmartSearchConfig(title) router.pushController( @@ -60,10 +53,6 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) { ) } - override fun onItemLongClick(position: Int) { - return - } - companion object { const val MANGA_ID = "manga_id" } diff --git a/app/src/main/java/exh/recs/RecommendsPager.kt b/app/src/main/java/exh/recs/RecommendsPagingSource.kt similarity index 95% rename from app/src/main/java/exh/recs/RecommendsPager.kt rename to app/src/main/java/exh/recs/RecommendsPagingSource.kt index dc8ad3be1..2db0bd2d4 100644 --- a/app/src/main/java/exh/recs/RecommendsPager.kt +++ b/app/src/main/java/exh/recs/RecommendsPagingSource.kt @@ -8,12 +8,13 @@ import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.system.logcat import exh.util.MangaType import exh.util.mangaType +import exh.util.nullIfEmpty import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -191,12 +192,12 @@ class Anilist : API("https://graphql.anilist.co/") { } } -open class RecommendsPager( +open class RecommendsPagingSource( private val manga: Manga, private val smart: Boolean = true, private var preferredApi: API = API.MYANIMELIST, -) : Pager() { - override suspend fun requestNextPage() { +) : BrowsePagingSource() { + override suspend fun requestNextPage(currentPage: Int): MangasPage { if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi val apiList = API_MAP.toList().sortedByDescending { it.first == preferredApi } @@ -210,15 +211,9 @@ open class RecommendsPager( logcat(LogPriority.ERROR, e) { key.toString() } null } - }.orEmpty() + }?.nullIfEmpty() ?: throw NoResultsException() - val mangasPage = MangasPage(recs, false) - - if (mangasPage.mangas.isNotEmpty()) { - onPageReceived(mangasPage) - } else { - throw NoResultsException() - } + return MangasPage(recs, false) } companion object { diff --git a/app/src/main/java/exh/recs/RecommendsPresenter.kt b/app/src/main/java/exh/recs/RecommendsPresenter.kt index 12111fea5..efbc1338a 100644 --- a/app/src/main/java/exh/recs/RecommendsPresenter.kt +++ b/app/src/main/java/exh/recs/RecommendsPresenter.kt @@ -1,10 +1,13 @@ package exh.recs +import android.os.Bundle +import androidx.paging.PagingSource import eu.kanade.domain.manga.interactor.GetManga import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter -import eu.kanade.tachiyomi.ui.browse.source.browse.Pager +import exh.metadata.metadata.base.RaisedSearchMetadata import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -20,8 +23,12 @@ class RecommendsPresenter( var manga: Manga? = null - override fun createPager(query: String, filters: FilterList): Pager { + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) this.manga = runBlocking { getManga.await(mangaId) } - return RecommendsPager(manga!!) + } + + override fun createPager(query: String, filters: FilterList): PagingSource> { + return RecommendsPagingSource(manga!!) } } diff --git a/app/src/main/res/color/source_comfortable_item_title.xml b/app/src/main/res/color/source_comfortable_item_title.xml deleted file mode 100644 index c64350682..000000000 --- a/app/src/main/res/color/source_comfortable_item_title.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/source_comfortable_grid_item.xml b/app/src/main/res/layout/source_comfortable_grid_item.xml deleted file mode 100644 index b86ddc316..000000000 --- a/app/src/main/res/layout/source_comfortable_grid_item.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/source_compact_grid_item.xml b/app/src/main/res/layout/source_compact_grid_item.xml deleted file mode 100644 index bb9009eb3..000000000 --- a/app/src/main/res/layout/source_compact_grid_item.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/source_controller.xml b/app/src/main/res/layout/source_controller.xml deleted file mode 100644 index 1338a925e..000000000 --- a/app/src/main/res/layout/source_controller.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/source_enhanced_ehentai_list_item.xml b/app/src/main/res/layout/source_enhanced_ehentai_list_item.xml deleted file mode 100644 index 5f3e7e5ba..000000000 --- a/app/src/main/res/layout/source_enhanced_ehentai_list_item.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/source_list_item.xml b/app/src/main/res/layout/source_list_item.xml deleted file mode 100644 index 03221dc01..000000000 --- a/app/src/main/res/layout/source_list_item.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/source_progress_item.xml b/app/src/main/res/layout/source_progress_item.xml deleted file mode 100644 index 5a2988e28..000000000 --- a/app/src/main/res/layout/source_progress_item.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/source_recycler_autofit.xml b/app/src/main/res/layout/source_recycler_autofit.xml deleted file mode 100644 index a3198c0b5..000000000 --- a/app/src/main/res/layout/source_recycler_autofit.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/app/src/main/res/menu/source_browse.xml b/app/src/main/res/menu/source_browse.xml deleted file mode 100644 index 28b72d8eb..000000000 --- a/app/src/main/res/menu/source_browse.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d874dbba6..51348d6fe 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -870,4 +870,5 @@ See your recently updated manga Widget not available when app lock is enabled + You are about to remove this manga from your library diff --git a/gradle/sy.versions.toml b/gradle/sy.versions.toml index d4f5cd6b8..5ee46b6a7 100644 --- a/gradle/sy.versions.toml +++ b/gradle/sy.versions.toml @@ -13,4 +13,5 @@ xlog = "com.elvishew:xlog:1.11.0" debugOverlay-standard = { module = "com.ms-square:debugoverlay", version.ref = "debugOverlay" } debugOverlay-noop = { module = "com.ms-square:debugoverlay-no-op", version.ref = "debugOverlay" } -ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" \ No newline at end of file +ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" +composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.2.3" \ No newline at end of file