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:
parent
d59d960c6a
commit
d2182ed380
@ -10,3 +10,13 @@ data class MangaCover(
|
|||||||
val url: String?,
|
val url: String?,
|
||||||
val lastModified: Long,
|
val lastModified: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun Manga.asMangaCover(): MangaCover {
|
||||||
|
return MangaCover(
|
||||||
|
mangaId = id,
|
||||||
|
sourceId = source,
|
||||||
|
isMangaFavorite = favorite,
|
||||||
|
url = thumbnailUrl,
|
||||||
|
lastModified = coverLastModified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import androidx.paging.LoadState
|
|||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.components.Badge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||||
@ -93,9 +94,7 @@ fun BrowseSourceComfortableGridItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// SY -->
|
// SY -->
|
||||||
coverBadgeEnd = {
|
coverBadgeEnd = {
|
||||||
|
@ -17,6 +17,7 @@ import androidx.paging.LoadState
|
|||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.components.Badge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
import eu.kanade.presentation.components.MangaCompactGridItem
|
||||||
@ -93,9 +94,7 @@ private fun BrowseSourceCompactGridItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// SY -->
|
// SY -->
|
||||||
coverBadgeEnd = {
|
coverBadgeEnd = {
|
||||||
|
@ -14,6 +14,7 @@ import androidx.paging.compose.LazyPagingItems
|
|||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.domain.manga.model.MangaCover
|
import eu.kanade.domain.manga.model.MangaCover
|
||||||
|
import eu.kanade.presentation.browse.InLibraryBadge
|
||||||
import eu.kanade.presentation.components.Badge
|
import eu.kanade.presentation.components.Badge
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
import eu.kanade.presentation.components.LazyColumn
|
||||||
@ -87,9 +88,7 @@ fun BrowseSourceListItem(
|
|||||||
),
|
),
|
||||||
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
coverAlpha = if (manga.favorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
badge = {
|
badge = {
|
||||||
if (manga.favorite) {
|
InLibraryBadge(enabled = manga.favorite)
|
||||||
Badge(text = stringResource(R.string.in_library))
|
|
||||||
}
|
|
||||||
// SY -->
|
// SY -->
|
||||||
if (metadata is MangaDexSearchMetadata) {
|
if (metadata is MangaDexSearchMetadata) {
|
||||||
metadata.followStatus?.let { followStatus ->
|
metadata.followStatus?.let { followStatus ->
|
||||||
|
@ -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) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,8 @@ class Padding {
|
|||||||
val medium = 16.dp
|
val medium = 16.dp
|
||||||
|
|
||||||
val small = 8.dp
|
val small = 8.dp
|
||||||
|
|
||||||
|
val tiny = 4.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val MaterialTheme.padding: Padding
|
val MaterialTheme.padding: Padding
|
||||||
|
@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
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.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
|
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.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
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.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.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
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 {
|
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen {
|
||||||
|
|
||||||
|
var newSelectedItem by mutableStateOf<Pair<Long, Long>?>(null)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val screenModel = rememberScreenModel { MigrationListScreenModel(config) }
|
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) {
|
LaunchedEffect(screenModel) {
|
||||||
screenModel.navigateOut.collect {
|
screenModel.navigateOut.collect {
|
||||||
if (navigator.canPop) {
|
if (navigator.canPop) {
|
||||||
@ -133,11 +144,8 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
|||||||
} else {
|
} else {
|
||||||
sources.filter { it.id != migrationItem.manga.source }
|
sources.filter { it.id != migrationItem.manga.source }
|
||||||
}
|
}
|
||||||
val searchController = SearchController(migrationItem.manga, validSources)
|
val searchScreen = MigrateSearchScreen(migrationItem.manga.id, validSources.map { it.id })
|
||||||
searchController.useMangaForMigration = { manga, source ->
|
navigator push searchScreen
|
||||||
screenModel.useMangaForMigration(context, manga, source, migrationItem.manga.id)
|
|
||||||
}
|
|
||||||
router.pushController(searchController)
|
|
||||||
},
|
},
|
||||||
migrateNow = { screenModel.migrateManga(it, false) },
|
migrateNow = { screenModel.migrateManga(it, false) },
|
||||||
copyNow = { screenModel.migrateManga(it, true) },
|
copyNow = { screenModel.migrateManga(it, true) },
|
||||||
|
@ -28,7 +28,6 @@ import eu.kanade.domain.track.interactor.InsertTrack
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
@ -383,14 +382,16 @@ class MigrationListScreenModel(
|
|||||||
updateManga.awaitAll(listOfNotNull(mangaUpdate, prevMangaUpdate))
|
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 }
|
val migratingManga = migratingItems.value.find { it.manga.id == selectedMangaId }
|
||||||
?: return
|
?: return
|
||||||
migratingManga.searchResult.value = SearchResult.Searching
|
migratingManga.searchResult.value = SearchResult.Searching
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
val result = migratingManga.migrationScope.async {
|
val result = migratingManga.migrationScope.async {
|
||||||
|
val manga = getManga(newMangaId)!!
|
||||||
val localManga = networkToLocalManga.await(manga)
|
val localManga = networkToLocalManga.await(manga)
|
||||||
try {
|
try {
|
||||||
|
val source = sourceManager.get(manga.source)!!
|
||||||
val chapters = source.getChapterList(localManga.toSManga())
|
val chapters = source.getChapterList(localManga.toSManga())
|
||||||
syncChaptersWithSource.await(chapters, localManga, source)
|
syncChaptersWithSource.await(chapters, localManga, source)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -14,7 +14,7 @@ import eu.kanade.presentation.browse.MigrateMangaScreen
|
|||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.util.LocalRouter
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import eu.kanade.tachiyomi.R
|
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.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -45,16 +45,14 @@ data class MigrationMangaScreen(
|
|||||||
state = state,
|
state = state,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
// SY -->
|
// SY -->
|
||||||
PreMigrationController.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
router,
|
navigator,
|
||||||
listOf(it.id),
|
listOf(it.id),
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
},
|
},
|
||||||
onClickCover = {
|
onClickCover = { navigator.push(MangaScreen(it.id)) },
|
||||||
navigator.push(MangaScreen(it.id))
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
@ -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)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,13 +7,9 @@ import androidx.core.os.bundleOf
|
|||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.presentation.browse.SourceSearchScreen
|
import eu.kanade.presentation.browse.SourceSearchScreen
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class SourceSearchController(
|
class SourceSearchController(
|
||||||
bundle: Bundle,
|
bundle: Bundle,
|
||||||
@ -27,7 +23,7 @@ class SourceSearchController(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
var useMangaForMigration: ((Manga, Source) -> Unit)? = null
|
var useMangaForMigration: ((Long) -> Unit)? = null
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
@ -37,10 +33,7 @@ class SourceSearchController(
|
|||||||
onFabClick = { filterSheet?.show() },
|
onFabClick = { filterSheet?.show() },
|
||||||
// SY -->
|
// SY -->
|
||||||
onMangaClick = { manga ->
|
onMangaClick = { manga ->
|
||||||
val sourceManager = Injekt.get<SourceManager>()
|
useMangaForMigration?.let { it(manga.id) }
|
||||||
val source = sourceManager.get(manga.source) ?: return@SourceSearchScreen
|
|
||||||
useMangaForMigration?.let { it(manga, source) }
|
|
||||||
router.popCurrentController()
|
|
||||||
router.popCurrentController()
|
router.popCurrentController()
|
||||||
},
|
},
|
||||||
// SY <--
|
// SY <--
|
||||||
|
@ -16,9 +16,8 @@ import eu.kanade.domain.manga.interactor.GetFavorites
|
|||||||
import eu.kanade.presentation.browse.MigrateSourceScreen
|
import eu.kanade.presentation.browse.MigrateSourceScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
import eu.kanade.presentation.util.LocalRouter
|
|
||||||
import eu.kanade.tachiyomi.R
|
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.ui.browse.migration.manga.MigrationMangaScreen
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
@ -29,9 +28,6 @@ import uy.kohesive.injekt.api.get
|
|||||||
fun Screen.migrateSourceTab(): TabContent {
|
fun Screen.migrateSourceTab(): TabContent {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
// SY -->
|
|
||||||
val router = LocalRouter.currentOrThrow
|
|
||||||
// SY <--
|
|
||||||
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
|
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
@ -63,9 +59,9 @@ fun Screen.migrateSourceTab(): TabContent {
|
|||||||
val sourceMangas =
|
val sourceMangas =
|
||||||
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
|
manga.asSequence().filter { it.source == source.id }.map { it.id }.toList()
|
||||||
withUIContext {
|
withUIContext {
|
||||||
PreMigrationController.navigateToMigration(
|
PreMigrationScreen.navigateToMigration(
|
||||||
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
|
||||||
router,
|
navigator,
|
||||||
sourceMangas,
|
sourceMangas,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -152,6 +152,8 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
val onDismissRequest = { presenter.dialog = null }
|
val onDismissRequest = { presenter.dialog = null }
|
||||||
when (val dialog = presenter.dialog) {
|
when (val dialog = presenter.dialog) {
|
||||||
|
null -> {}
|
||||||
|
is Dialog.Migrate -> {}
|
||||||
is Dialog.AddDuplicateManga -> {
|
is Dialog.AddDuplicateManga -> {
|
||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
@ -182,7 +184,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
null -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(onBack = ::navigateUp)
|
BackHandler(onBack = ::navigateUp)
|
||||||
|
@ -429,6 +429,7 @@ open class BrowseSourcePresenter(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: List<CheckboxState.State<Category>>,
|
val initialSelection: List<CheckboxState.State<Category>>,
|
||||||
) : Dialog()
|
) : Dialog()
|
||||||
|
data class Migrate(val newManga: Manga) : Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
|
@ -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"
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,227 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import android.view.Menu
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import android.view.MenuInflater
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
import android.view.MenuItem
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
class GlobalSearchController(
|
||||||
* This controller shows and manages the different search result in global search.
|
val searchQuery: String = "",
|
||||||
* This controller should only handle UI actions, IO actions should be done by [GlobalSearchPresenter]
|
val extensionFilter: String = "",
|
||||||
* [GlobalSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
|
) : BasicFullComposeController() {
|
||||||
*/
|
|
||||||
open class GlobalSearchController(
|
|
||||||
protected val initialQuery: String? = null,
|
|
||||||
protected val extensionFilter: String? = null,
|
|
||||||
bundle: Bundle? = null,
|
|
||||||
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
|
|
||||||
GlobalSearchCardAdapter.OnMangaClickListener,
|
|
||||||
GlobalSearchAdapter.OnTitleClickListener {
|
|
||||||
|
|
||||||
private val preferences: BasePreferences by injectLazy()
|
@Composable
|
||||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
override fun ComposeContent() {
|
||||||
|
CompositionLocalProvider(LocalRouter provides router) {
|
||||||
/**
|
Navigator(
|
||||||
* Adapter containing search results grouped by lang.
|
screen = GlobalSearchScreen(
|
||||||
*/
|
searchQuery = searchQuery,
|
||||||
protected var adapter: GlobalSearchAdapter? = null
|
extensionFilter = extensionFilter,
|
||||||
|
),
|
||||||
/**
|
|
||||||
* 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -223,7 +223,7 @@ object LibraryScreen : Screen {
|
|||||||
},
|
},
|
||||||
onRefresh = onClickRefresh,
|
onRefresh = onClickRefresh,
|
||||||
onGlobalSearchClicked = {
|
onGlobalSearchClicked = {
|
||||||
router.pushController(GlobalSearchController(screenModel.state.value.searchQuery))
|
router.pushController(GlobalSearchController(screenModel.state.value.searchQuery ?: ""))
|
||||||
},
|
},
|
||||||
getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
|
getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
|
||||||
getDisplayModeForPage = { state.categories[it].display },
|
getDisplayModeForPage = { state.categories[it].display },
|
||||||
|
@ -520,7 +520,7 @@ class MainActivity : BaseActivity() {
|
|||||||
if (router.backstackSize > 1) {
|
if (router.backstackSize > 1) {
|
||||||
router.popToRoot()
|
router.popToRoot()
|
||||||
}
|
}
|
||||||
router.pushController(GlobalSearchController(query, filter))
|
router.pushController(GlobalSearchController(query, filter ?: ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -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.popControllerWithTag
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
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
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController.Companion.SMART_SEARCH_SOURCE_TAG
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesController.Companion.SMART_SEARCH_SOURCE_TAG
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
@ -137,7 +137,12 @@ class MangaScreen(
|
|||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
onBackClicked = router::popCurrentController,
|
onBackClicked = {
|
||||||
|
when {
|
||||||
|
navigator.canPop -> navigator.pop()
|
||||||
|
else -> router.popCurrentController()
|
||||||
|
}
|
||||||
|
},
|
||||||
onChapterClicked = { openChapter(context, it) },
|
onChapterClicked = { openChapter(context, it) },
|
||||||
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadChapter = screenModel::runChapterDownloadActions.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onAddToLibraryClicked = {
|
onAddToLibraryClicked = {
|
||||||
@ -164,8 +169,8 @@ class MangaScreen(
|
|||||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
||||||
onMigrateClicked = { migrateManga(router, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
||||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||||
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
|
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
|
||||||
onRecommendClicked = { openRecommends(context, router, screenModel.source?.getMainSource(), successState.manga) },
|
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
|
* Copy Manga URL to Clipboard
|
||||||
*/
|
*/
|
||||||
@ -446,6 +438,19 @@ class MangaScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// 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) {
|
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
|
||||||
navigator.push(MetadataViewScreen(manga.id, manga.source))
|
navigator.push(MetadataViewScreen(manga.id, manga.source))
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is BrowseSourcePresenter.Dialog.Migrate -> Unit
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
||||||
|
|
||||||
|
|
@ -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>
|
|
Loading…
x
Reference in New Issue
Block a user