diff --git a/app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt b/app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt index 748926bb6..6b2d6a42f 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/MangaCover.kt @@ -10,3 +10,13 @@ data class MangaCover( val url: String?, val lastModified: Long, ) + +fun Manga.asMangaCover(): MangaCover { + return MangaCover( + mangaId = id, + sourceId = source, + isMangaFavorite = favorite, + url = thumbnailUrl, + lastModified = coverLastModified, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseBadges.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseBadges.kt new file mode 100644 index 000000000..3c7ab0c27 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseBadges.kt @@ -0,0 +1,13 @@ +package eu.kanade.presentation.browse + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import eu.kanade.presentation.components.Badge +import eu.kanade.tachiyomi.R + +@Composable +fun InLibraryBadge(enabled: Boolean) { + if (enabled) { + Badge(text = stringResource(R.string.in_library)) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt new file mode 100644 index 000000000..014039448 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -0,0 +1,111 @@ +package eu.kanade.presentation.browse + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.components.GlobalSearchCardRow +import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem +import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem +import eu.kanade.presentation.browse.components.GlobalSearchResultItem +import eu.kanade.presentation.browse.components.GlobalSearchToolbar +import eu.kanade.presentation.components.LazyColumn +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.padding +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItemResult +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState +import eu.kanade.tachiyomi.util.system.LocaleHelper + +@Composable +fun GlobalSearchScreen( + state: GlobalSearchState, + navigateUp: () -> Unit, + onChangeSearchQuery: (String?) -> Unit, + onSearch: (String) -> Unit, + getManga: @Composable (CatalogueSource, Manga) -> State, + onClickSource: (CatalogueSource) -> Unit, + onClickItem: (Manga) -> Unit, + onLongClickItem: (Manga) -> Unit, +) { + Scaffold( + topBar = { + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + ) + }, + ) { paddingValues -> + GlobalSearchContent( + items = state.items, + contentPadding = paddingValues, + getManga = getManga, + onClickSource = onClickSource, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + ) + } +} + +@Composable +fun GlobalSearchContent( + items: Map, + contentPadding: PaddingValues, + getManga: @Composable (CatalogueSource, Manga) -> State, + onClickSource: (CatalogueSource) -> Unit, + onClickItem: (Manga) -> Unit, + onLongClickItem: (Manga) -> Unit, +) { + LazyColumn( + contentPadding = contentPadding, + ) { + items.forEach { (source, result) -> + item { + GlobalSearchResultItem( + title = source.name, + subtitle = LocaleHelper.getDisplayName(source.lang), + onClick = { onClickSource(source) }, + ) { + when (result) { + is GlobalSearchItemResult.Error -> { + GlobalSearchErrorResultItem(message = result.throwable.message) + } + GlobalSearchItemResult.Loading -> { + GlobalSearchLoadingResultItem() + } + is GlobalSearchItemResult.Success -> { + if (result.isEmpty) { + Text( + text = stringResource(id = R.string.no_results_found), + modifier = Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + ) + return@GlobalSearchResultItem + } + + GlobalSearchCardRow( + titles = result.result, + getManga = { getManga(source, it) }, + onClick = onClickItem, + onLongClick = onLongClickItem, + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt new file mode 100644 index 000000000..436c9bc2b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -0,0 +1,100 @@ +package eu.kanade.presentation.browse + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.browse.components.GlobalSearchCardRow +import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem +import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem +import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem +import eu.kanade.presentation.browse.components.GlobalSearchResultItem +import eu.kanade.presentation.browse.components.GlobalSearchToolbar +import eu.kanade.presentation.components.LazyColumn +import eu.kanade.presentation.components.Scaffold +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItemResult +import eu.kanade.tachiyomi.util.system.LocaleHelper + +@Composable +fun MigrateSearchScreen( + navigateUp: () -> Unit, + state: MigrateSearchState, + getManga: @Composable (CatalogueSource, Manga) -> State, + onChangeSearchQuery: (String?) -> Unit, + onSearch: (String) -> Unit, + onClickSource: (CatalogueSource) -> Unit, + onClickItem: (Manga) -> Unit, + onLongClickItem: (Manga) -> Unit, +) { + Scaffold( + topBar = { + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + ) + }, + ) { paddingValues -> + MigrateSearchContent( + sourceId = state.manga?.source ?: -1, + items = state.items, + contentPadding = paddingValues, + getManga = getManga, + onClickSource = onClickSource, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + ) + } +} + +@Composable +fun MigrateSearchContent( + sourceId: Long, + items: Map, + contentPadding: PaddingValues, + getManga: @Composable (CatalogueSource, Manga) -> State, + onClickSource: (CatalogueSource) -> Unit, + onClickItem: (Manga) -> Unit, + onLongClickItem: (Manga) -> Unit, +) { + LazyColumn( + contentPadding = contentPadding, + ) { + items.forEach { (source, result) -> + item { + GlobalSearchResultItem( + title = if (source.id == sourceId) "▶ ${source.name}" else source.name, + subtitle = LocaleHelper.getDisplayName(source.lang), + onClick = { onClickSource(source) }, + ) { + when (result) { + is GlobalSearchItemResult.Error -> { + GlobalSearchErrorResultItem(message = result.throwable.message) + } + GlobalSearchItemResult.Loading -> { + GlobalSearchLoadingResultItem() + } + is GlobalSearchItemResult.Success -> { + if (result.isEmpty) { + GlobalSearchEmptyResultItem() + return@GlobalSearchResultItem + } + + GlobalSearchCardRow( + titles = result.result, + getManga = { getManga(source, it) }, + onClick = onClickItem, + onLongClick = onLongClickItem, + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt index 912881f9c..72e20d2ed 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt @@ -17,6 +17,7 @@ import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.components.Badge import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.MangaComfortableGridItem @@ -93,9 +94,7 @@ fun BrowseSourceComfortableGridItem( ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverBadgeStart = { - if (manga.favorite) { - Badge(text = stringResource(R.string.in_library)) - } + InLibraryBadge(enabled = manga.favorite) }, // SY --> coverBadgeEnd = { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt index d1ea7ef23..ce35105ca 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt @@ -17,6 +17,7 @@ import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.components.Badge import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.MangaCompactGridItem @@ -93,9 +94,7 @@ private fun BrowseSourceCompactGridItem( ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, coverBadgeStart = { - if (manga.favorite) { - Badge(text = stringResource(R.string.in_library)) - } + InLibraryBadge(enabled = manga.favorite) }, // SY --> coverBadgeEnd = { diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt index fb3dac2a8..8627699db 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt @@ -14,6 +14,7 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.items import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.presentation.browse.InLibraryBadge import eu.kanade.presentation.components.Badge import eu.kanade.presentation.components.CommonMangaItemDefaults import eu.kanade.presentation.components.LazyColumn @@ -87,9 +88,7 @@ fun BrowseSourceListItem( ), coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, badge = { - if (manga.favorite) { - Badge(text = stringResource(R.string.in_library)) - } + InLibraryBadge(enabled = manga.favorite) // SY --> if (metadata is MangaDexSearchMetadata) { metadata.followStatus?.let { followStatus -> diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt new file mode 100644 index 000000000..7b6887fc5 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -0,0 +1,40 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.asMangaCover +import eu.kanade.presentation.util.padding + +@Composable +fun GlobalSearchCardRow( + titles: List, + getManga: @Composable (Manga) -> State, + onClick: (Manga) -> Unit, + onLongClick: (Manga) -> Unit, +) { + LazyRow( + contentPadding = PaddingValues( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + items(titles) { title -> + val title by getManga(title) + GlobalSearchCard( + title = title.title, + cover = title.asMangaCover(), + isFavorite = title.favorite, + onClick = { onClick(title) }, + onLongClick = { onLongClick(title) }, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt new file mode 100644 index 000000000..ba4ec27b7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -0,0 +1,101 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.material.icons.outlined.Error +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.kanade.presentation.util.padding +import eu.kanade.tachiyomi.R + +@Composable +fun GlobalSearchResultItem( + title: String, + subtitle: String, + onClick: () -> Unit, + content: @Composable () -> Unit, +) { + Column { + Row( + modifier = Modifier + .padding( + start = MaterialTheme.padding.medium, + end = MaterialTheme.padding.tiny, + ) + .fillMaxWidth() + .clickable(onClick = onClick), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + Text(text = subtitle) + } + IconButton(onClick = onClick) { + Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null) + } + } + content() + } +} + +@Composable +fun GlobalSearchEmptyResultItem() { + Text( + text = stringResource(id = R.string.no_results_found), + modifier = Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + ) +} + +@Composable +fun GlobalSearchLoadingResultItem() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = MaterialTheme.padding.medium), + ) { + CircularProgressIndicator( + modifier = Modifier + .size(16.dp) + .align(Alignment.Center), + strokeWidth = 2.dp, + ) + } +} + +@Composable +fun GlobalSearchErrorResultItem(message: String?) { + Column( + modifier = Modifier + .padding(vertical = MaterialTheme.padding.medium) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon(imageVector = Icons.Outlined.Error, contentDescription = null) + Text(text = message ?: stringResource(id = R.string.unknown_error)) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt new file mode 100644 index 000000000..c05a1c9e4 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -0,0 +1,36 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.SearchToolbar + +@Composable +fun GlobalSearchToolbar( + searchQuery: String?, + progress: Int, + total: Int, + navigateUp: () -> Unit, + onChangeSearchQuery: (String?) -> Unit, + onSearch: (String) -> Unit, +) { + Box { + SearchToolbar( + searchQuery = searchQuery, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + navigateUp = navigateUp, + ) + if (progress in 1 until total) { + LinearProgressIndicator( + progress = progress / total.toFloat(), + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth(), + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSerachCard.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSerachCard.kt new file mode 100644 index 000000000..7279b8bb1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSerachCard.kt @@ -0,0 +1,33 @@ +package eu.kanade.presentation.browse.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.presentation.browse.InLibraryBadge +import eu.kanade.presentation.components.CommonMangaItemDefaults +import eu.kanade.presentation.components.MangaComfortableGridItem + +@Composable +fun GlobalSearchCard( + title: String, + cover: MangaCover, + isFavorite: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { + Box(modifier = Modifier.width(128.dp)) { + MangaComfortableGridItem( + title = title, + coverData = cover, + coverBadgeStart = { + InLibraryBadge(enabled = isFavorite) + }, + coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f, + onClick = onClick, + onLongClick = onLongClick, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/util/Constants.kt b/app/src/main/java/eu/kanade/presentation/util/Constants.kt index 246e7f071..651ad0853 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Constants.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Constants.kt @@ -18,6 +18,8 @@ class Padding { val medium = 16.dp val small = 8.dp + + val tiny = 4.dp } val MaterialTheme.padding: Padding diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt index b66957f6a..0ed4c8c1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreen.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen @@ -16,11 +18,10 @@ import eu.kanade.presentation.browse.components.MigrationMangaDialog import eu.kanade.presentation.util.LocalRouter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler -import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen -import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController +import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreen import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.lang.withUIContext @@ -28,6 +29,8 @@ import eu.kanade.tachiyomi.util.system.toast class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen { + var newSelectedItem by mutableStateOf?>(null) + @Composable override fun Content() { val screenModel = rememberScreenModel { MigrationListScreenModel(config) } @@ -58,6 +61,14 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen } } + LaunchedEffect(newSelectedItem) { + if (newSelectedItem != null) { + val (oldId, newId) = newSelectedItem!! + screenModel.useMangaForMigration(context, newId, oldId) + newSelectedItem = null + } + } + LaunchedEffect(screenModel) { screenModel.navigateOut.collect { if (navigator.canPop) { @@ -133,11 +144,8 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen } else { sources.filter { it.id != migrationItem.manga.source } } - val searchController = SearchController(migrationItem.manga, validSources) - searchController.useMangaForMigration = { manga, source -> - screenModel.useMangaForMigration(context, manga, source, migrationItem.manga.id) - } - router.pushController(searchController) + val searchScreen = MigrateSearchScreen(migrationItem.manga.id, validSources.map { it.id }) + navigator push searchScreen }, migrateNow = { screenModel.migrateManga(it, false) }, copyNow = { screenModel.migrateManga(it, true) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt index a262ab196..0a9f112dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationListScreenModel.kt @@ -28,7 +28,6 @@ import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.source.online.all.EHentai @@ -383,14 +382,16 @@ class MigrationListScreenModel( updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate)) } - fun useMangaForMigration(context: Context, manga: Manga, source: Source, selectedMangaId: Long) { + fun useMangaForMigration(context: Context, newMangaId: Long, selectedMangaId: Long) { val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId } ?: return migratingManga.searchResult.value = SearchResult.Searching coroutineScope.launchIO { val result = migratingManga.migrationScope.async { + val manga = getManga(newMangaId)!! val localManga = networkToLocalManga.await(manga) try { + val source = sourceManager.get(manga.source)!! val chapters = source.getChapterList(localManga.toSManga()) syncChaptersWithSource.await(chapters, localManga, source) } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt index a7ce70547..3f853bb2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt @@ -14,7 +14,7 @@ import eu.kanade.presentation.browse.MigrateMangaScreen import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.util.LocalRouter import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.flow.collectLatest @@ -45,16 +45,14 @@ data class MigrationMangaScreen( state = state, onClickItem = { // SY --> - PreMigrationController.navigateToMigration( + PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), - router, + navigator, listOf(it.id), ) // SY <-- }, - onClickCover = { - navigator.push(MangaScreen(it.id)) - }, + onClickCover = { navigator.push(MangaScreen(it.id)) }, ) LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt new file mode 100644 index 000000000..d3738802f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -0,0 +1,64 @@ +package eu.kanade.tachiyomi.ui.browse.migration.search + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.browse.MigrateSearchScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen +import eu.kanade.tachiyomi.ui.manga.MangaScreen + +class MigrateSearchScreen(private val mangaId: Long, private val validSources: List) : Screen { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val router = LocalRouter.currentOrThrow + val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId, validSources = validSources) } + val state by screenModel.state.collectAsState() + // SY --> + val migrationScreen = remember { + navigator.items.filterIsInstance().last() + } + // SY <-- + + MigrateSearchScreen( + navigateUp = navigator::pop, + state = state, + getManga = { source, manga -> + screenModel.getManga(source = source, initialManga = manga) + }, + onChangeSearchQuery = screenModel::updateSearchQuery, + onSearch = screenModel::search, + onClickSource = { + if (!screenModel.incognitoMode.get()) { + screenModel.lastUsedSourceId.set(it.id) + } + // SY --> + router.pushController( + SourceSearchController(state.manga!!, it, state.searchQuery) + .also { searchController -> + searchController.useMangaForMigration = { newMangaId -> + migrationScreen.newSelectedItem = mangaId to newMangaId + navigator.pop() + } + }, + ) + // SY <-- + }, + onClickItem = { + // SY --> + migrationScreen.newSelectedItem = mangaId to it.id + navigator.pop() + // SY <-- + }, + onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt new file mode 100644 index 000000000..c957ad520 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -0,0 +1,80 @@ +package eu.kanade.tachiyomi.ui.browse.migration.search + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.base.BasePreferences +import eu.kanade.domain.manga.interactor.GetManga +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItemResult +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MigrateSearchScreenModel( + val mangaId: Long, + // SY --> + val validSources: List, + // SY <-- + initialExtensionFilter: String = "", + preferences: BasePreferences = Injekt.get(), + private val sourcePreferences: SourcePreferences = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val getManga: GetManga = Injekt.get(), +) : SearchScreenModel(MigrateSearchState()) { + + init { + extensionFilter = initialExtensionFilter + coroutineScope.launch { + val manga = getManga.await(mangaId)!! + + mutableState.update { + it.copy(manga = manga, searchQuery = manga.title) + } + + search(manga.title) + } + } + + val incognitoMode = preferences.incognitoMode() + val lastUsedSourceId = sourcePreferences.lastUsedSource() + + override fun getEnabledSources(): List { + // SY --> + return validSources.mapNotNull { sourceManager.get(it) } + .filterIsInstance() + // SY <-- + } + + override fun updateSearchQuery(query: String?) { + mutableState.update { + it.copy(searchQuery = query) + } + } + + override fun updateItems(items: Map) { + mutableState.update { + it.copy(items = items) + } + } + + override fun getItems(): Map { + return mutableState.value.items + } +} + +@Immutable +data class MigrateSearchState( + val manga: Manga? = null, + val searchQuery: String? = null, + val items: Map = emptyMap(), +) { + + val progress: Int = items.count { it.value !is GlobalSearchItemResult.Loading } + + val total: Int = items.size +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt deleted file mode 100644 index f18132e98..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ /dev/null @@ -1,81 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.search - -import android.os.Bundle -import androidx.core.os.bundleOf -import eu.kanade.domain.manga.interactor.GetManga -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter -import kotlinx.coroutines.runBlocking -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class SearchController( - private var manga: Manga? = null, - private var sources: List? = null, -) : GlobalSearchController( - manga?.ogTitle, - bundle = bundleOf( - OLD_MANGA to manga?.id, - SOURCES to sources?.map { it.id }?.toLongArray(), - ), -) { - constructor(mangaId: Long, sources: LongArray) : - this( - runBlocking { - Injekt.get() - .await(mangaId) - }, - sources.map { Injekt.get().getOrStub(it) }.filterIsInstance(), - ) - - @Suppress("unused") - constructor(bundle: Bundle) : this( - bundle.getLong(OLD_MANGA), - bundle.getLongArray(SOURCES) ?: LongArray(0), - ) - - var useMangaForMigration: ((Manga, Source) -> Unit)? = null - - /** - * Called when controller is initialized. - */ - init { - setHasOptionsMenu(true) - } - - override fun createPresenter(): GlobalSearchPresenter { - return SearchPresenter( - initialQuery, - manga!!, - sources, - ) - } - - override fun onMangaClick(manga: Manga) { - val sourceManager = Injekt.get() - val source = sourceManager.get(manga.source) ?: return - useMangaForMigration?.let { it(manga, source) } - router.popCurrentController() - } - - override fun onMangaLongClick(manga: Manga) { - // Call parent's default click listener - super.onMangaClick(manga) - } - - override fun onTitleClick(source: CatalogueSource) { - presenter.sourcePreferences.lastUsedSource().set(source.id) - - router.pushController(SourceSearchController(manga!!, source, presenter.query).also { it.useMangaForMigration = useMangaForMigration }) - } - - companion object { - const val OLD_MANGA = "old_manga" - const val SOURCES = "sources" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt deleted file mode 100644 index 2db075fa4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.migration.search -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem -import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter - -class SearchPresenter( - initialQuery: String? = "", - private val manga: Manga, - sources: List? = null, -) : GlobalSearchPresenter(initialQuery, sourcesToUse = sources) { - - override fun getEnabledSources(): List { - // Put the source of the selected manga at the top - return super.getEnabledSources() - .sortedByDescending { it.id == manga.source } - } - - override fun createCatalogueSearchItem(source: CatalogueSource, results: List?): GlobalSearchItem { - // Set the catalogue search item as highlighted if the source matches that of the selected manga - return GlobalSearchItem(source, results, source.id == manga.source) - } -} 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 9c0cf59a3..a29277977 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 @@ -7,13 +7,9 @@ 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.Source -import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get class SourceSearchController( bundle: Bundle, @@ -27,7 +23,7 @@ class SourceSearchController( ), ) - var useMangaForMigration: ((Manga, Source) -> Unit)? = null + var useMangaForMigration: ((Long) -> Unit)? = null @Composable override fun ComposeContent() { @@ -37,10 +33,7 @@ class SourceSearchController( onFabClick = { filterSheet?.show() }, // SY --> onMangaClick = { manga -> - val sourceManager = Injekt.get() - val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen - useMangaForMigration?.let { it(manga, source) } - router.popCurrentController() + useMangaForMigration?.let { it(manga.id) } router.popCurrentController() }, // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt index 2ba9c403a..60c4785a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrateSourceTab.kt @@ -16,9 +16,8 @@ import eu.kanade.domain.manga.interactor.GetFavorites import eu.kanade.presentation.browse.MigrateSourceScreen import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent -import eu.kanade.presentation.util.LocalRouter import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaScreen import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext @@ -29,9 +28,6 @@ import uy.kohesive.injekt.api.get fun Screen.migrateSourceTab(): TabContent { val uriHandler = LocalUriHandler.current val navigator = LocalNavigator.currentOrThrow - // SY --> - val router = LocalRouter.currentOrThrow - // SY <-- val screenModel = rememberScreenModel { MigrateSourceScreenModel() } val state by screenModel.state.collectAsState() @@ -63,9 +59,9 @@ fun Screen.migrateSourceTab(): TabContent { val sourceMangas = manga.asSequence().filter { it.source == source.id }.map { it.id }.toList() withUIContext { - PreMigrationController.navigateToMigration( + PreMigrationScreen.navigateToMigration( Injekt.get().skipPreMigration().get(), - router, + navigator, sourceMangas, ) } 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 657b16b5e..af95ce77c 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 @@ -152,6 +152,8 @@ open class BrowseSourceController(bundle: Bundle) : val onDismissRequest = { presenter.dialog = null } when (val dialog = presenter.dialog) { + null -> {} + is Dialog.Migrate -> {} is Dialog.AddDuplicateManga -> { DuplicateMangaDialog( onDismissRequest = onDismissRequest, @@ -182,7 +184,6 @@ open class BrowseSourceController(bundle: Bundle) : }, ) } - null -> {} } BackHandler(onBack = ::navigateUp) 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 22a9a17c2..74d138370 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 @@ -429,6 +429,7 @@ open class BrowseSourcePresenter( val manga: Manga, val initialSelection: List>, ) : Dialog() + data class Migrate(val newManga: Manga) : Dialog() } // EXH --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt deleted file mode 100644 index d0fc3e29c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.os.Bundle -import android.os.Parcelable -import android.util.SparseArray -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.source.CatalogueSource - -/** - * Adapter that holds the search cards. - * - * @param controller instance of [GlobalSearchController]. - */ -class GlobalSearchAdapter(val controller: GlobalSearchController) : - FlexibleAdapter(null, controller, true) { - - val titleClickListener: OnTitleClickListener = controller - - /** - * Bundle where the view state of the holders is saved. - */ - private var bundle = Bundle() - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { - super.onBindViewHolder(holder, position, payloads) - restoreHolderState(holder) - } - - override fun onViewRecycled(holder: RecyclerView.ViewHolder) { - super.onViewRecycled(holder) - saveHolderState(holder, bundle) - } - - override fun onSaveInstanceState(outState: Bundle) { - val holdersBundle = Bundle() - allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) } - outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle) - super.onSaveInstanceState(outState) - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)!! - } - - /** - * Saves the view state of the given holder. - * - * @param holder The holder to save. - * @param outState The bundle where the state is saved. - */ - private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) { - val key = "holder_${holder.bindingAdapterPosition}" - val holderState = SparseArray() - holder.itemView.saveHierarchyState(holderState) - outState.putSparseParcelableArray(key, holderState) - } - - /** - * Restores the view state of the given holder. - * - * @param holder The holder to restore. - */ - private fun restoreHolderState(holder: RecyclerView.ViewHolder) { - val key = "holder_${holder.bindingAdapterPosition}" - val holderState = bundle.getSparseParcelableArray(key) - if (holderState != null) { - holder.itemView.restoreHierarchyState(holderState) - bundle.remove(key) - } - } - - interface OnTitleClickListener { - fun onTitleClick(source: CatalogueSource) - } -} - -private const val HOLDER_BUNDLE_KEY = "holder_bundle" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardAdapter.kt deleted file mode 100644 index 3c620a6ff..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardAdapter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.domain.manga.model.Manga - -/** - * Adapter that holds the manga items from search results. - * - * @param controller instance of [GlobalSearchController]. - */ -class GlobalSearchCardAdapter(controller: GlobalSearchController) : - FlexibleAdapter(null, controller, true) { - - /** - * Listen for browse item clicks. - */ - val mangaClickListener: OnMangaClickListener = controller - - /** - * Listener which should be called when user clicks browse. - * Note: Should only be handled by [GlobalSearchController] - */ - interface OnMangaClickListener { - fun onMangaClick(manga: Manga) - fun onMangaLongClick(manga: Manga) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt deleted file mode 100644 index 4a5222063..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt +++ /dev/null @@ -1,58 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.view.View -import androidx.core.view.isVisible -import coil.dispose -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher -import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding -import eu.kanade.tachiyomi.util.view.loadAutoPause - -class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) : - FlexibleViewHolder(view, adapter) { - - private val binding = GlobalSearchControllerCardItemBinding.bind(view) - - init { - // Call onMangaClickListener when item is pressed. - itemView.setOnClickListener { - val item = adapter.getItem(bindingAdapterPosition) - if (item != null) { - adapter.mangaClickListener.onMangaClick(item.manga) - } - } - itemView.setOnLongClickListener { - val item = adapter.getItem(bindingAdapterPosition) - if (item != null) { - adapter.mangaClickListener.onMangaLongClick(item.manga) - } - true - } - } - - fun bind(manga: Manga) { - binding.card.clipToOutline = true - - // Set manga title - binding.title.text = manga.title - - // Set alpha of thumbnail. - binding.cover.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) - } - - fun setImage(manga: Manga) { - binding.cover.dispose() - binding.cover.loadAutoPause(manga) { - setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardItem.kt deleted file mode 100644 index 5296d2d6d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardItem.kt +++ /dev/null @@ -1,40 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -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 - -class GlobalSearchCardItem(val manga: Manga) : AbstractFlexibleItem() { - - override fun getLayoutRes(): Int { - return R.layout.global_search_controller_card_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): GlobalSearchCardHolder { - return GlobalSearchCardHolder(view, adapter as GlobalSearchCardAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: GlobalSearchCardHolder, - position: Int, - payloads: List?, - ) { - holder.bind(manga) - } - - override fun equals(other: Any?): Boolean { - if (other is GlobalSearchCardItem) { - 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/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt index f4c8f2862..94c534112 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt @@ -1,227 +1,25 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch -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 androidx.appcompat.widget.SearchView -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import dev.chrisbanes.insetter.applyInsetter -import eu.kanade.domain.base.BasePreferences -import eu.kanade.domain.manga.model.Manga -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController -import eu.kanade.tachiyomi.ui.base.controller.pushController -import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController -import eu.kanade.tachiyomi.ui.manga.MangaController -import uy.kohesive.injekt.injectLazy +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import cafe.adriel.voyager.navigator.Navigator +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController -/** - * This controller shows and manages the different search result in global search. - * This controller should only handle UI actions, IO actions should be done by [GlobalSearchPresenter] - * [GlobalSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search - */ -open class GlobalSearchController( - protected val initialQuery: String? = null, - protected val extensionFilter: String? = null, - bundle: Bundle? = null, -) : SearchableNucleusController(bundle), - GlobalSearchCardAdapter.OnMangaClickListener, - GlobalSearchAdapter.OnTitleClickListener { +class GlobalSearchController( + val searchQuery: String = "", + val extensionFilter: String = "", +) : BasicFullComposeController() { - private val preferences: BasePreferences by injectLazy() - private val sourcePreferences: SourcePreferences by injectLazy() - - /** - * Adapter containing search results grouped by lang. - */ - protected var adapter: GlobalSearchAdapter? = null - - /** - * Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu - */ - private var optionsMenuSearchItem: MenuItem? = null - - init { - setHasOptionsMenu(true) - } - - override fun createBinding(inflater: LayoutInflater) = GlobalSearchControllerBinding.inflate(inflater) - - override fun getTitle(): String? { - return presenter.query - } - - override fun createPresenter(): GlobalSearchPresenter { - return GlobalSearchPresenter(initialQuery, extensionFilter) - } - - /** - * Called when manga in global search is clicked, opens manga. - * - * @param manga clicked item containing manga information. - */ - override fun onMangaClick(manga: Manga) { - router.pushController(MangaController(manga.id, true)) - } - - /** - * Called when manga in global search is long clicked. - * - * @param manga clicked item containing manga information. - */ - override fun onMangaLongClick(manga: Manga) { - // Delegate to single click by default. - onMangaClick(manga) - } - - /** - * Adds items to the options menu. - * - * @param menu menu containing options. - * @param inflater used to load the menu xml. - */ - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - createOptionsMenu( - menu, - inflater, - R.menu.global_search, - R.id.action_search, - ) - - optionsMenuSearchItem = menu.findItem(R.id.action_search) - - // Focus search on launch from browse screen - if (initialQuery.isNullOrEmpty()) { - optionsMenuSearchItem?.expandActionView() + @Composable + override fun ComposeContent() { + CompositionLocalProvider(LocalRouter provides router) { + Navigator( + screen = GlobalSearchScreen( + searchQuery = searchQuery, + extensionFilter = extensionFilter, + ), + ) } } - - override fun onSearchMenuItemActionCollapse(item: MenuItem?) { - super.onSearchMenuItemActionCollapse(item) - // Close this screen if query is empty - // i.e. launch from browse screen and clicking the back button icon without making any search - if (presenter.query.isEmpty()) { - router.popCurrentController() - } - } - - override fun onSearchMenuItemActionExpand(item: MenuItem?) { - super.onSearchMenuItemActionExpand(item) - val searchView = optionsMenuSearchItem?.actionView as SearchView - searchView.onActionViewExpanded() // Required to show the query in the view - - if (nonSubmittedQuery.isBlank()) { - searchView.setQuery(presenter.query, false) - } - } - - override fun onSearchViewQueryTextSubmit(query: String?) { - presenter.search(query ?: "") - optionsMenuSearchItem?.collapseActionView() - setTitle() // Update toolbar title - } - - /** - * Called when the view is created - * - * @param view view of controller - */ - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - adapter = GlobalSearchAdapter(this) - - // Create recycler and set adapter. - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.adapter = adapter - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - override fun onSaveViewState(view: View, outState: Bundle) { - super.onSaveViewState(view, outState) - adapter?.onSaveInstanceState(outState) - } - - override fun onRestoreViewState(view: View, savedViewState: Bundle) { - super.onRestoreViewState(view, savedViewState) - adapter?.onRestoreInstanceState(savedViewState) - } - - /** - * Returns the view holder for the given manga. - * - * @param source used to find holder containing source - * @return the holder of the manga or null if it's not bound. - */ - private fun getHolder(source: CatalogueSource): GlobalSearchHolder? { - val adapter = adapter ?: return null - - adapter.allBoundViewHolders.forEach { holder -> - val item = adapter.getItem(holder.bindingAdapterPosition) - if (item != null && source.id == item.source.id) { - return holder as GlobalSearchHolder - } - } - - return null - } - - /** - * Add search result to adapter. - * - * @param searchResult result of search. - */ - fun setItems(searchResult: List) { - if (searchResult.isEmpty() && sourcePreferences.searchPinnedSourcesOnly().get()) { - binding.emptyView.show(R.string.no_pinned_sources) - } else { - binding.emptyView.hide() - } - - adapter?.updateDataSet(searchResult) - - val progress = searchResult.mapNotNull { it.results }.size.toDouble() / searchResult.size - if (progress < 1) { - binding.progressBar.isVisible = true - binding.progressBar.progress = (progress * 100).toInt() - } else { - binding.progressBar.isVisible = false - } - } - - /** - * Called from the presenter when a manga is initialized. - * - * @param manga the initialized manga. - */ - fun onMangaInitialized(source: CatalogueSource, manga: Manga) { - getHolder(source)?.setImage(manga) - } - - /** - * Opens a catalogue with the given search. - */ - override fun onTitleClick(source: CatalogueSource) { - if (!preferences.incognitoMode().get()) { - sourcePreferences.lastUsedSource().set(source.id) - } - router.pushController(BrowseSourceController(source, presenter.query)) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchHolder.kt deleted file mode 100644 index 7bfd9327b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchHolder.kt +++ /dev/null @@ -1,110 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.view.View -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.domain.manga.model.Manga -import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardBinding -import eu.kanade.tachiyomi.source.LocalSource -import eu.kanade.tachiyomi.util.system.LocaleHelper - -/** - * Holder that binds the [GlobalSearchItem] containing catalogue cards. - * - * @param view view of [GlobalSearchItem] - * @param adapter instance of [GlobalSearchAdapter] - */ -class GlobalSearchHolder(view: View, val adapter: GlobalSearchAdapter) : - FlexibleViewHolder(view, adapter) { - - private val binding = GlobalSearchControllerCardBinding.bind(view) - - /** - * Adapter containing manga from search results. - */ - private val mangaAdapter = GlobalSearchCardAdapter(adapter.controller) - - private var lastBoundResults: List? = null - - init { - // Set layout horizontal. - binding.recycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false) - binding.recycler.adapter = mangaAdapter - - binding.titleWrapper.setOnClickListener { - adapter.getItem(bindingAdapterPosition)?.let { - adapter.titleClickListener.onTitleClick(it.source) - } - } - } - - /** - * Show the loading of source search result. - * - * @param item item of card. - */ - fun bind(item: GlobalSearchItem) { - val source = item.source - val results = item.results - - val titlePrefix = if (item.highlighted) "▶ " else "" - - binding.title.text = titlePrefix + source.name - binding.subtitle.isVisible = source !is LocalSource - binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) - - when { - results == null -> { - binding.progress.isVisible = true - showResultsHolder() - } - results.isEmpty() -> { - binding.progress.isVisible = false - showNoResults() - } - else -> { - binding.progress.isVisible = false - showResultsHolder() - } - } - if (results !== lastBoundResults) { - mangaAdapter.updateDataSet(results) - lastBoundResults = results - } - } - - /** - * Called from the presenter when a manga is initialized. - * - * @param manga the initialized manga. - */ - fun setImage(manga: Manga) { - getHolder(manga)?.setImage(manga) - } - - /** - * 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): GlobalSearchCardHolder? { - mangaAdapter.allBoundViewHolders.forEach { holder -> - val item = mangaAdapter.getItem(holder.bindingAdapterPosition) - if (item != null && item.manga.id == manga.id) { - return holder as GlobalSearchCardHolder - } - } - - return null - } - - private fun showResultsHolder() { - binding.noResultsFound.isVisible = false - } - - private fun showNoResults() { - binding.noResultsFound.isVisible = true - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchItem.kt deleted file mode 100644 index b2df301c4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchItem.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.CatalogueSource - -/** - * Item that contains search result information. - * - * @param source the source for the search results. - * @param results the search results. - * @param highlighted whether this search item should be highlighted/marked in the catalogue search view. - */ -class GlobalSearchItem(val source: CatalogueSource, val results: List?, val highlighted: Boolean = false) : - AbstractFlexibleItem() { - - /** - * Set view. - * - * @return id of view - */ - override fun getLayoutRes(): Int { - return R.layout.global_search_controller_card - } - - /** - * Create view holder (see [GlobalSearchAdapter]. - * - * @return holder of view. - */ - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): GlobalSearchHolder { - return GlobalSearchHolder(view, adapter as GlobalSearchAdapter) - } - - /** - * Bind item to view. - */ - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: GlobalSearchHolder, - position: Int, - payloads: List?, - ) { - holder.bind(this) - } - - /** - * Used to check if two items are equal. - * - * @return items are equal? - */ - override fun equals(other: Any?): Boolean { - if (other is GlobalSearchItem) { - return source.id == other.source.id - } - return false - } - - /** - * Return hash code of item. - * - * @return hashcode - */ - override fun hashCode(): Int { - return source.id.toInt() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt deleted file mode 100644 index ce6ee8a04..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt +++ /dev/null @@ -1,269 +0,0 @@ -package eu.kanade.tachiyomi.ui.browse.source.globalsearch - -import android.os.Bundle -import eu.kanade.domain.base.BasePreferences -import eu.kanade.domain.manga.interactor.NetworkToLocalManga -import eu.kanade.domain.manga.interactor.UpdateManga -import eu.kanade.domain.manga.model.toDbManga -import eu.kanade.domain.manga.model.toDomainManga -import eu.kanade.domain.manga.model.toMangaUpdate -import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.toDomainManga -import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -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 -import eu.kanade.tachiyomi.util.lang.runAsObservable -import eu.kanade.tachiyomi.util.system.logcat -import kotlinx.coroutines.runBlocking -import logcat.LogPriority -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import rx.subjects.PublishSubject -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import eu.kanade.domain.manga.model.Manga as DomainManga - -open class GlobalSearchPresenter( - private val initialQuery: String? = "", - private val initialExtensionFilter: String? = null, - private val sourcesToUse: List? = null, - val sourceManager: SourceManager = Injekt.get(), - val preferences: BasePreferences = Injekt.get(), - val sourcePreferences: SourcePreferences = Injekt.get(), - private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), - private val updateManga: UpdateManga = Injekt.get(), -) : BasePresenter() { - - /** - * Enabled sources. - */ - val sources by lazy { getSourcesToQuery() } - - /** - * Fetches the different sources by user settings. - */ - private var fetchSourcesSubscription: Subscription? = null - - /** - * Subject which fetches image of given manga. - */ - private val fetchImageSubject = PublishSubject.create, Source>>() - - /** - * Subscription for fetching images of manga. - */ - private var fetchImageSubscription: Subscription? = null - - private val extensionManager: ExtensionManager by injectLazy() - - private var extensionFilter: String? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - extensionFilter = savedState?.getString(GlobalSearchPresenter::extensionFilter.name) - ?: initialExtensionFilter - - // Perform a search with previous or initial state - search( - savedState?.getString(BrowseSourcePresenter::query.name) - ?: initialQuery.orEmpty(), - ) - } - - override fun onDestroy() { - fetchSourcesSubscription?.unsubscribe() - fetchImageSubscription?.unsubscribe() - super.onDestroy() - } - - override fun onSave(state: Bundle) { - state.putString(BrowseSourcePresenter::query.name, query) - state.putString(GlobalSearchPresenter::extensionFilter.name, extensionFilter) - super.onSave(state) - } - - /** - * Returns a list of enabled sources ordered by language and name, with pinned sources - * prioritized. - * - * @return list containing enabled sources. - */ - protected open fun getEnabledSources(): List { - val languages = sourcePreferences.enabledLanguages().get() - val disabledSourceIds = sourcePreferences.disabledSources().get() - val pinnedSourceIds = sourcePreferences.pinnedSources().get() - - return sourceManager.getVisibleCatalogueSources() - .filter { it.lang in languages } - .filterNot { it.id.toString() in disabledSourceIds } - .sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name.lowercase()} (${it.lang})" })) - } - - private fun getSourcesToQuery(): List { - if (sourcesToUse != null) return sourcesToUse - val filter = extensionFilter - val enabledSources = getEnabledSources() - var filteredSources: List? = null - - if (!filter.isNullOrEmpty()) { - // SY --> - val filteredSourceIds = extensionManager.installedExtensionsFlow.value - .filter { it.pkgName == filter } - .flatMap { it.sources } - .map { it.id } - filteredSources = enabledSources.filter { it.id in filteredSourceIds } - // SY <-- - } - - if (filteredSources != null && filteredSources.isNotEmpty()) { - return filteredSources - } - - val onlyPinnedSources = sourcePreferences.searchPinnedSourcesOnly().get() - val pinnedSourceIds = sourcePreferences.pinnedSources().get() - - return enabledSources - .filter { if (onlyPinnedSources) it.id.toString() in pinnedSourceIds else true } - } - - /** - * Creates a catalogue search item - */ - protected open fun createCatalogueSearchItem(source: CatalogueSource, results: List?): GlobalSearchItem { - return GlobalSearchItem(source, results) - } - - /** - * Initiates a search for manga per catalogue. - * - * @param query query on which to search. - */ - fun search(query: String) { - // Return if there's nothing to do - if (this.query == query) return - - // Update query - this.query = query - - // Create image fetch subscription - initializeFetchImageSubscription() - - // Create items with the initial state - val initialItems = sources.map { createCatalogueSearchItem(it, null) } - var items = initialItems - - val pinnedSourceIds = sourcePreferences.pinnedSources().get() - - fetchSourcesSubscription?.unsubscribe() - fetchSourcesSubscription = Observable.from(sources) - .flatMap( - { source -> - Observable.defer { source.fetchSearchManga(1, query, source.getFilterList()) } - .subscribeOn(Schedulers.io()) - .onErrorReturn { MangasPage(emptyList(), false) } // Ignore timeouts or other exceptions - .map { it.mangas } - .map { list -> list.map { runBlocking { networkToLocalManga(it, source.id) } } } // Convert to local manga - .doOnNext { fetchImage(it, source) } // Load manga covers - .map { list -> createCatalogueSearchItem(source, list.map { GlobalSearchCardItem(it) }) } - }, - 5, - ) - .observeOn(AndroidSchedulers.mainThread()) - // Update matching source with the obtained results - .map { result -> - items - .map { item -> if (item.source == result.source) result else item } - .sortedWith( - compareBy( - // Bubble up sources that actually have results - { it.results.isNullOrEmpty() }, - // Same as initial sort, i.e. pinned first then alphabetically - { it.source.id.toString() !in pinnedSourceIds }, - { "${it.source.name.lowercase()} (${it.source.lang})" }, - ), - ) - } - // Update current state - .doOnNext { items = it } - // Deliver initial state - .startWith(initialItems) - .subscribeLatestCache( - { view, manga -> - view.setItems(manga) - }, - { _, error -> - logcat(LogPriority.ERROR, error) - }, - ) - } - - /** - * Initialize a list of manga. - * - * @param manga the list of manga to initialize. - */ - private fun fetchImage(manga: List, source: Source) { - fetchImageSubject.onNext(Pair(manga, source)) - } - - /** - * Subscribes to the initializer of manga details and updates the view if needed. - */ - private fun initializeFetchImageSubscription() { - fetchImageSubscription?.unsubscribe() - fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) - .flatMap { (first, source) -> - Observable.from(first) - .filter { it.thumbnailUrl == null && !it.initialized } - .map { Pair(it, source) } - .concatMap { runAsObservable { getMangaDetails(it.first.toDbManga(), it.second) } } - .map { Pair(source as CatalogueSource, it) } - } - .onBackpressureBuffer() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { (source, manga) -> - @Suppress("DEPRECATION") - view?.onMangaInitialized(source, manga.toDomainManga()!!) - }, - { error -> - logcat(LogPriority.ERROR, error) - }, - ) - } - - /** - * Initializes the given manga. - * - * @param manga the manga to initialize. - * @return The initialized manga. - */ - private suspend fun getMangaDetails(manga: Manga, source: Source): Manga { - val networkManga = source.getMangaDetails(manga.copy()) - manga.copyFrom(networkManga) - manga.initialized = true - updateManga.await(manga.toDomainManga()!!.toMangaUpdate()) - return manga - } - - /** - * 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. - * - * @param sManga the manga from the source. - * @return a manga from the database. - */ - protected open suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): DomainManga { - return networkToLocalManga.await(sManga.toDomainManga(sourceId)) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt new file mode 100644 index 000000000..48eed0a69 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -0,0 +1,53 @@ +package eu.kanade.tachiyomi.ui.browse.source.globalsearch + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.browse.GlobalSearchScreen +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.ui.manga.MangaController + +class GlobalSearchScreen( + val searchQuery: String = "", + val extensionFilter: String = "", +) : Screen { + + @Composable + override fun Content() { + val router = LocalRouter.currentOrThrow + + val screenModel = rememberScreenModel { + GlobalSearchScreenModel( + initialQuery = searchQuery, + initialExtensionFilter = extensionFilter, + ) + } + val state by screenModel.state.collectAsState() + + GlobalSearchScreen( + state = state, + navigateUp = router::popCurrentController, + onChangeSearchQuery = screenModel::updateSearchQuery, + onSearch = screenModel::search, + getManga = { source, manga -> + screenModel.getManga( + source = source, + initialManga = manga, + ) + }, + onClickSource = { + if (!screenModel.incognitoMode.get()) { + screenModel.lastUsedSourceId.set(it.id) + } + router.pushController(BrowseSourceController(it, state.searchQuery)) + }, + onClickItem = { router.pushController(MangaController(it.id, true)) }, + onLongClickItem = { router.pushController(MangaController(it.id, true)) }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt new file mode 100644 index 000000000..d4ff54755 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt @@ -0,0 +1,96 @@ +package eu.kanade.tachiyomi.ui.browse.source.globalsearch + +import androidx.compose.runtime.Immutable +import eu.kanade.domain.base.BasePreferences +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.SourceManager +import kotlinx.coroutines.flow.update +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class GlobalSearchScreenModel( + initialQuery: String = "", + initialExtensionFilter: String = "", + preferences: BasePreferences = Injekt.get(), + private val sourcePreferences: SourcePreferences = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), +) : SearchScreenModel(GlobalSearchState(searchQuery = initialQuery)) { + + val incognitoMode = preferences.incognitoMode() + val lastUsedSourceId = sourcePreferences.lastUsedSource() + + init { + extensionFilter = initialExtensionFilter + if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) { + search(initialQuery) + } + } + + override fun getEnabledSources(): List { + val enabledLanguages = sourcePreferences.enabledLanguages().get() + val disabledSources = sourcePreferences.disabledSources().get() + val pinnedSources = sourcePreferences.pinnedSources().get() + + // SY --> + val shouldSearchPinnedOnly = sourcePreferences.searchPinnedSourcesOnly().get() + // SY <-- + + return sourceManager.getCatalogueSources() + .filter { it.lang in enabledLanguages } + .filterNot { "${it.id}" in disabledSources } + .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) + // SY --> + .filter { + if (shouldSearchPinnedOnly) { + "${it.id}" in pinnedSources + } else { + true + } + } + // SY <-- + } + + override fun updateSearchQuery(query: String?) { + mutableState.update { + it.copy(searchQuery = query) + } + } + + override fun updateItems(items: Map) { + mutableState.update { + it.copy(items = items) + } + } + + override fun getItems(): Map { + return mutableState.value.items + } +} + +sealed class GlobalSearchItemResult { + object Loading : GlobalSearchItemResult() + + data class Error( + val throwable: Throwable, + ) : GlobalSearchItemResult() + + data class Success( + val result: List, + ) : GlobalSearchItemResult() { + val isEmpty: Boolean + get() = result.isEmpty() + } +} + +@Immutable +data class GlobalSearchState( + val searchQuery: String? = null, + val items: Map = emptyMap(), +) { + + val progress: Int = items.count { it.value !is GlobalSearchItemResult.Loading } + + val total: Int = items.size +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt new file mode 100644 index 000000000..225dd22a7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -0,0 +1,163 @@ +package eu.kanade.tachiyomi.ui.browse.source.globalsearch + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.manga.interactor.GetManga +import eu.kanade.domain.manga.interactor.NetworkToLocalManga +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.toDomainManga +import eu.kanade.domain.manga.model.toMangaUpdate +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.extension.ExtensionManager +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.util.lang.awaitSingle +import eu.kanade.tachiyomi.util.lang.withIOContext +import eu.kanade.tachiyomi.util.lang.withNonCancellableContext +import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import logcat.LogPriority +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.Executors + +abstract class SearchScreenModel( + initialState: T, + private val sourcePreferences: SourcePreferences = Injekt.get(), + private val extensionManager: ExtensionManager = Injekt.get(), + private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), +) : StateScreenModel(initialState) { + + private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() + + protected var query: String? = null + protected lateinit var extensionFilter: String + + private val sources by lazy { getSelectedSources() } + + @Composable + fun getManga(source: CatalogueSource, initialManga: Manga): State { + return produceState(initialValue = initialManga) { + getManga.subscribe(initialManga.url, initialManga.source) + .collectLatest { manga -> + if (manga == null) return@collectLatest + withIOContext { + initializeManga(source, manga) + } + value = manga + } + } + } + + /** + * Initialize a manga. + * + * @param source to interact with + * @param manga to initialize. + */ + private suspend fun initializeManga(source: CatalogueSource, manga: Manga) { + if (manga.thumbnailUrl != null || manga.initialized) return + withNonCancellableContext { + try { + val networkManga = source.getMangaDetails(manga.toSManga()) + val updatedManga = manga.copyFrom(networkManga) + .copy(initialized = true) + + updateManga.await(updatedManga.toMangaUpdate()) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } + } + } + + abstract fun getEnabledSources(): List + + fun getSelectedSources(): List { + val filter = extensionFilter + + val enabledSources = getEnabledSources() + + // SY --> + if (filter.isEmpty()) { + return enabledSources + } + // SY <-- + + // SY --> + val filteredSourceIds = extensionManager.installedExtensionsFlow.value + .filter { it.pkgName == filter } + .flatMap { it.sources } + .filterIsInstance() + .map { it.id } + return enabledSources.filter { it.id in filteredSourceIds } + // SY <-- + } + + abstract fun updateSearchQuery(query: String?) + + abstract fun updateItems(items: Map) + + abstract fun getItems(): Map + + fun getAndUpdateItems(function: (Map) -> Map) { + updateItems(function(getItems())) + } + + fun search(query: String) { + if (this.query == query) return + + this.query = query + + val initialItems = getSelectedSources().associateWith { GlobalSearchItemResult.Loading } + updateItems(initialItems) + + val pinnedSources = sourcePreferences.pinnedSources().get() + + val comparator = { mutableMap: MutableMap -> + compareBy( + { mutableMap[it] is GlobalSearchItemResult.Success }, + { "${it.id}" in pinnedSources }, + { "${it.name.lowercase()} (${it.lang})" }, + ) + } + + coroutineScope.launch { + sources.forEach { source -> + val page = try { + withContext(coroutineDispatcher) { + source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle() + } + } catch (e: Exception) { + getAndUpdateItems { items -> + val mutableMap = items.toMutableMap() + mutableMap[source] = GlobalSearchItemResult.Error(throwable = e) + mutableMap.toSortedMap(comparator(mutableMap)) + mutableMap.toMap() + } + return@forEach + } + + val titles = page.mangas.map { + withIOContext { + networkToLocalManga.await(it.toDomainManga(source.id)) + } + } + + getAndUpdateItems { items -> + val mutableMap = items.toMutableMap() + mutableMap[source] = GlobalSearchItemResult.Success(titles) + mutableMap.toSortedMap(comparator(mutableMap)) + mutableMap.toMap() + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt index 1f81ba0f6..7df73842d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt @@ -223,7 +223,7 @@ object LibraryScreen : Screen { }, onRefresh = onClickRefresh, onGlobalSearchClicked = { - router.pushController(GlobalSearchController(screenModel.state.value.searchQuery)) + router.pushController(GlobalSearchController(screenModel.state.value.searchQuery ?: "")) }, getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) }, getDisplayModeForPage = { state.categories[it].display }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 225c3a1f0..eb520285f 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -520,7 +520,7 @@ class MainActivity : BaseActivity() { if (router.backstackSize > 1) { router.popToRoot() } - router.pushController(GlobalSearchController(query, filter)) + router.pushController(GlobalSearchController(query, filter ?: "")) } } else -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index e5d134817..f9d47eba0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -59,7 +59,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController +import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationScreen import eu.kanade.tachiyomi.ui.browse.source.SourcesController import eu.kanade.tachiyomi.ui.browse.source.SourcesController.Companion.SMART_SEARCH_SOURCE_TAG import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController @@ -137,7 +137,12 @@ class MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, isTabletUi = isTabletUi(), - onBackClicked = router::popCurrentController, + onBackClicked = { + when { + navigator.canPop -> navigator.pop() + else -> router.popCurrentController() + } + }, onChapterClicked = { openChapter(context, it) }, onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() }, onAddToLibraryClicked = { @@ -164,8 +169,8 @@ class MangaScreen( onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite }, - onMigrateClicked = { migrateManga(router, screenModel.manga!!) }.takeIf { successState.manga.favorite }, // SY --> + onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite }, onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) }, onEditInfoClicked = screenModel::showEditMangaInfoDialog, onRecommendClicked = { openRecommends(context, router, screenModel.source?.getMainSource(), successState.manga) }, @@ -422,19 +427,6 @@ class MangaScreen( } } - /** - * Initiates source migration for the specific manga. - */ - private fun migrateManga(router: Router, manga: Manga) { - // SY --> - PreMigrationController.navigateToMigration( - Injekt.get().skipPreMigration().get(), - router, - listOf(manga.id), - ) - // SY <-- - } - /** * Copy Manga URL to Clipboard */ @@ -446,6 +438,19 @@ class MangaScreen( } // SY --> + /** + * Initiates source migration for the specific manga. + */ + private fun migrateManga(navigator: Navigator, manga: Manga) { + // SY --> + PreMigrationScreen.navigateToMigration( + Injekt.get().skipPreMigration().get(), + navigator, + listOf(manga.id), + ) + // SY <-- + } + private fun openMetadataViewer(navigator: Navigator, manga: Manga) { navigator.push(MetadataViewScreen(manga.id, manga.source)) } diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt index 5f2926ddf..e3dbd95c3 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt @@ -86,6 +86,7 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) }, ) } + is BrowseSourcePresenter.Dialog.Migrate -> Unit null -> {} } } diff --git a/app/src/main/res/layout/global_search_controller.xml b/app/src/main/res/layout/global_search_controller.xml deleted file mode 100644 index 7505a9825..000000000 --- a/app/src/main/res/layout/global_search_controller.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/global_search_controller_card.xml b/app/src/main/res/layout/global_search_controller_card.xml deleted file mode 100644 index 0d35b90db..000000000 --- a/app/src/main/res/layout/global_search_controller_card.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/global_search_controller_card_item.xml b/app/src/main/res/layout/global_search_controller_card_item.xml deleted file mode 100644 index 9acf49483..000000000 --- a/app/src/main/res/layout/global_search_controller_card_item.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/global_search.xml b/app/src/main/res/menu/global_search.xml deleted file mode 100644 index f3fdb1067..000000000 --- a/app/src/main/res/menu/global_search.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - -