Use Compose on Global/Migrate Search screen (#8631)

* Use Compose on Global/Migrate Search screen

- Refactor to use Voyager and Compose
- Use sealed class for state
- Somethings are broken/missing due to screens using different navigation libraries

* Review changes

(cherry picked from commit f99b62a069e8e987318a0144090560795d59e3ff)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceComfortableGrid.kt
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceCompactGrid.kt
#	app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceList.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
This commit is contained in:
Andreas 2022-11-27 20:56:21 +01:00 committed by Jobobby04
parent d59d960c6a
commit d2182ed380
42 changed files with 980 additions and 1272 deletions

View File

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

View File

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

View File

@ -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<Manga>,
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<CatalogueSource, GlobalSearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
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,
)
}
}
}
}
}
}
}

View File

@ -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<Manga>,
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<CatalogueSource, GlobalSearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
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,
)
}
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Manga>,
getManga: @Composable (Manga) -> State<Manga>,
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) },
)
}
}
}

View File

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

View File

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

View File

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

View File

@ -18,6 +18,8 @@ class Padding {
val medium = 16.dp
val small = 8.dp
val tiny = 4.dp
}
val MaterialTheme.padding: Padding

View File

@ -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<Pair<Long, Long>?>(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) },

View File

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

View File

@ -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<UnsortedPreferences>().skipPreMigration().get(),
router,
navigator,
listOf(it.id),
)
// SY <--
},
onClickCover = {
navigator.push(MangaScreen(it.id))
},
onClickCover = { navigator.push(MangaScreen(it.id)) },
)
LaunchedEffect(Unit) {

View File

@ -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<Long>) : 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<MigrationListScreen>().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)) },
)
}
}

View File

@ -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<Long>,
// 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>(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<CatalogueSource> {
// SY -->
return validSources.mapNotNull { sourceManager.get(it) }
.filterIsInstance<CatalogueSource>()
// SY <--
}
override fun updateSearchQuery(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
}
}
override fun updateItems(items: Map<CatalogueSource, GlobalSearchItemResult>) {
mutableState.update {
it.copy(items = items)
}
}
override fun getItems(): Map<CatalogueSource, GlobalSearchItemResult> {
return mutableState.value.items
}
}
@Immutable
data class MigrateSearchState(
val manga: Manga? = null,
val searchQuery: String? = null,
val items: Map<CatalogueSource, GlobalSearchItemResult> = emptyMap(),
) {
val progress: Int = items.count { it.value !is GlobalSearchItemResult.Loading }
val total: Int = items.size
}

View File

@ -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<CatalogueSource>? = 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<GetManga>()
.await(mangaId)
},
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>(),
)
@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<SourceManager>()
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"
}
}

View File

@ -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<CatalogueSource>? = null,
) : GlobalSearchPresenter(initialQuery, sourcesToUse = sources) {
override fun getEnabledSources(): List<CatalogueSource> {
// 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<GlobalSearchCardItem>?): 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)
}
}

View File

@ -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<SourceManager>()
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
useMangaForMigration?.let { it(manga, source) }
router.popCurrentController()
useMangaForMigration?.let { it(manga.id) }
router.popCurrentController()
},
// SY <--

View File

@ -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<UnsortedPreferences>().skipPreMigration().get(),
router,
navigator,
sourceMangas,
)
}

View File

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

View File

@ -429,6 +429,7 @@ open class BrowseSourcePresenter(
val manga: Manga,
val initialSelection: List<CheckboxState.State<Category>>,
) : Dialog()
data class Migrate(val newManga: Manga) : Dialog()
}
// EXH -->

View File

@ -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<GlobalSearchItem>(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<Any?>) {
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<Parcelable>()
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<Parcelable>(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"

View File

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

View File

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

View File

@ -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<GlobalSearchCardHolder>() {
override fun getLayoutRes(): Int {
return R.layout.global_search_controller_card_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): GlobalSearchCardHolder {
return GlobalSearchCardHolder(view, adapter as GlobalSearchCardAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: GlobalSearchCardHolder,
position: Int,
payloads: List<Any?>?,
) {
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()
}
}

View File

@ -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<GlobalSearchControllerBinding, GlobalSearchPresenter>(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<GlobalSearchItem>) {
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))
}
}

View File

@ -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<GlobalSearchCardItem>? = 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
}
}

View File

@ -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<GlobalSearchCardItem>?, val highlighted: Boolean = false) :
AbstractFlexibleItem<GlobalSearchHolder>() {
/**
* 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<IFlexible<RecyclerView.ViewHolder>>): GlobalSearchHolder {
return GlobalSearchHolder(view, adapter as GlobalSearchAdapter)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: GlobalSearchHolder,
position: Int,
payloads: List<Any?>?,
) {
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()
}
}

View File

@ -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<CatalogueSource>? = 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<GlobalSearchController>() {
/**
* 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<Pair<List<DomainManga>, 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<CatalogueSource> {
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<CatalogueSource> {
if (sourcesToUse != null) return sourcesToUse
val filter = extensionFilter
val enabledSources = getEnabledSources()
var filteredSources: List<CatalogueSource>? = 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<GlobalSearchCardItem>?): 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<DomainManga>, 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))
}
}

View File

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

View File

@ -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>(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<CatalogueSource> {
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<CatalogueSource, GlobalSearchItemResult>) {
mutableState.update {
it.copy(items = items)
}
}
override fun getItems(): Map<CatalogueSource, GlobalSearchItemResult> {
return mutableState.value.items
}
}
sealed class GlobalSearchItemResult {
object Loading : GlobalSearchItemResult()
data class Error(
val throwable: Throwable,
) : GlobalSearchItemResult()
data class Success(
val result: List<Manga>,
) : GlobalSearchItemResult() {
val isEmpty: Boolean
get() = result.isEmpty()
}
}
@Immutable
data class GlobalSearchState(
val searchQuery: String? = null,
val items: Map<CatalogueSource, GlobalSearchItemResult> = emptyMap(),
) {
val progress: Int = items.count { it.value !is GlobalSearchItemResult.Loading }
val total: Int = items.size
}

View File

@ -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<T>(
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<T>(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<Manga> {
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<CatalogueSource>
fun getSelectedSources(): List<CatalogueSource> {
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<CatalogueSource>()
.map { it.id }
return enabledSources.filter { it.id in filteredSourceIds }
// SY <--
}
abstract fun updateSearchQuery(query: String?)
abstract fun updateItems(items: Map<CatalogueSource, GlobalSearchItemResult>)
abstract fun getItems(): Map<CatalogueSource, GlobalSearchItemResult>
fun getAndUpdateItems(function: (Map<CatalogueSource, GlobalSearchItemResult>) -> Map<CatalogueSource, GlobalSearchItemResult>) {
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<CatalogueSource, GlobalSearchItemResult> ->
compareBy<CatalogueSource>(
{ 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()
}
}
}
}
}

View File

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

View File

@ -520,7 +520,7 @@ class MainActivity : BaseActivity() {
if (router.backstackSize > 1) {
router.popToRoot()
}
router.pushController(GlobalSearchController(query, filter))
router.pushController(GlobalSearchController(query, filter ?: ""))
}
}
else -> {

View File

@ -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<UnsortedPreferences>().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<UnsortedPreferences>().skipPreMigration().get(),
navigator,
listOf(manga.id),
)
// SY <--
}
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
navigator.push(MetadataViewScreen(manga.id, manga.source))
}

View File

@ -86,6 +86,7 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
},
)
}
is BrowseSourcePresenter.Dialog.Migrate -> Unit
null -> {}
}
}

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
tools:listitem="@layout/global_search_controller_card" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
android:max="100"
android:visibility="gone"
tools:progress="50"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.75"
android:background="?attr/colorSurface" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toTopOf="@+id/subtitle"
app:layout_constraintEnd_toStartOf="@+id/title_more_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textSize="12sp"
android:visibility="gone"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/title_more_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="English"
tools:visibility="visible" />
<ImageView
android:id="@+id/title_more_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/all"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_forward_24dp"
app:tint="?android:attr/textColorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/no_results_found"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="@string/no_results_found"
android:visibility="gone" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
style="@style/Widget.Tachiyomi.CircularProgressIndicator.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:clipToPadding="false"
tools:listitem="@layout/global_search_controller_card_item" />
</LinearLayout>

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:background="@drawable/library_item_selector"
android:padding="4dp">
<FrameLayout
android:id="@+id/card"
android:layout_width="112dp"
android:layout_height="144dp"
android:background="@drawable/rounded_rectangle"
app:layout_constraintDimensionRatio="h,5:7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
style="@style/Widget.Tachiyomi.CircularProgressIndicator.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
<ImageView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />
<LinearLayout
android:id="@+id/badges"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/rounded_rectangle">
<TextView
android:id="@+id/favorite_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorSecondary"
android:maxLines="1"
android:paddingStart="3dp"
android:paddingTop="1dp"
android:paddingEnd="3dp"
android:paddingBottom="1dp"
android:fontFamily="sans-serif-condensed"
android:text="@string/in_library"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSecondary"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:padding="4dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="@+id/card"
app:layout_constraintStart_toStartOf="@+id/card"
app:layout_constraintTop_toBottomOf="@+id/card"
tools:text="Sample name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,12 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/action_search"
app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="collapseActionView|ifRoom" />
</menu>