refactor(migration): clean up migration list logic and UI components (#1403)
* Merged `MigrationItem`, `MigrationItemResult`, and `MigrationActionIcon` into `MigrationListItem` variants within `MigrationListScreenContent.kt` for better cohesion. * Simplified `MigrationListScreen` and `MigrationListScreenModel` by removing `MigrationProcedureConfig` and `MigrationType` in favor of direct parameters (`mangaIds`, `extraSearchQuery`). * Renamed `newSelectedItem` to `matchOverride` in migration screens. * Updated `SmartSourceSearchEngine` and `BaseSmartSearchEngine` method names for clarity: `smartSearch` -> `regularSearch` and `normalSearch` -> `deepSearch`. * Refactored search result states: `SearchResult.Result` is now `SearchResult.Success`. * Improved `MigrationMangaDialog` and `MigrationExitDialog` by standardizing parameter names and structure. * Inlined `BrowseSourceFloatingActionButton` logic into `SourceFeedScreen` and `MigrateSourceSearchScreen` using standard `SmallExtendedFloatingActionButton`. * Cleaned up various internal field names and logic related to "unmatched" vs "not found" entries.
This commit is contained in:
parent
e2a92b9220
commit
bc581e1d4b
19 changed files with 538 additions and 667 deletions
|
|
@ -4,7 +4,11 @@ import androidx.compose.animation.Crossfade
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
|
|
@ -12,7 +16,6 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
|
|
@ -31,6 +34,7 @@ import tachiyomi.domain.source.model.FeedSavedSearch
|
|||
import tachiyomi.domain.source.model.SavedSearch
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||
|
|
@ -156,10 +160,21 @@ fun SourceFeedScreen(
|
|||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
BrowseSourceFloatingActionButton(
|
||||
isVisible = hasFilters,
|
||||
onFabClick = onFabClick,
|
||||
// KMK -->
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
text = if (hasFilters) {
|
||||
stringResource(MR.strings.action_filter)
|
||||
} else {
|
||||
stringResource(SYMR.strings.saved_searches)
|
||||
},
|
||||
)
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
|
||||
onClick = onFabClick,
|
||||
)
|
||||
// KMK <--
|
||||
},
|
||||
) { paddingValues ->
|
||||
Crossfade(targetState = isLoading, label = "source_feed") { state ->
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceFloatingActionButton(
|
||||
isVisible: Boolean,
|
||||
onFabClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SmallExtendedFloatingActionButton(
|
||||
modifier = modifier,
|
||||
text = {
|
||||
Text(
|
||||
text = if (isVisible) {
|
||||
stringResource(MR.strings.action_filter)
|
||||
} else {
|
||||
stringResource(SYMR.strings.saved_searches)
|
||||
},
|
||||
)
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = "") },
|
||||
onClick = onFabClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mihon.feature.migration.list.models.MigratingManga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MigrationActionIcon(
|
||||
modifier: Modifier,
|
||||
result: MigratingManga.SearchResult,
|
||||
skipManga: () -> Unit,
|
||||
// KMK -->
|
||||
cancelManga: () -> Unit,
|
||||
// KMK <--
|
||||
searchManually: () -> Unit,
|
||||
migrateNow: () -> Unit,
|
||||
copyNow: () -> Unit,
|
||||
) {
|
||||
var moreExpanded by remember { mutableStateOf(false) }
|
||||
val closeMenu = { moreExpanded = false }
|
||||
|
||||
Box(modifier) {
|
||||
if (result is MigratingManga.SearchResult.Searching) {
|
||||
// KMK -->
|
||||
IconButton(onClick = cancelManga) {
|
||||
// KMK <--
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(SYMR.strings.action_stop),
|
||||
)
|
||||
}
|
||||
} else if (result is MigratingManga.SearchResult.Result || result is MigratingManga.SearchResult.NotFound) {
|
||||
IconButton(onClick = { moreExpanded = !moreExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(MR.strings.action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = moreExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
offset = DpOffset(8.dp, (-56).dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_search_manually)) },
|
||||
onClick = {
|
||||
searchManually()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_skip_entry)) },
|
||||
onClick = {
|
||||
skipManga()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
if (result is MigratingManga.SearchResult.Result) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_migrate_now)) },
|
||||
onClick = {
|
||||
migrateNow()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_copy_now)) },
|
||||
onClick = {
|
||||
copyNow()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import mihon.feature.migration.list.models.MigratingManga
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MigrationItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
sourcesString: String,
|
||||
chapterInfo: MigratingManga.ChapterInfo,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(4.dp),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
Modifier.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
data = manga,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
0f to Color.Transparent,
|
||||
1f to Color(0xAA000000),
|
||||
),
|
||||
)
|
||||
.fillMaxHeight(0.33f)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.BottomStart),
|
||||
text = manga.title.ifBlank { stringResource(MR.strings.unknown) },
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 18.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
color = Color.White,
|
||||
shadow = Shadow(
|
||||
color = Color.Black,
|
||||
blurRadius = 4f,
|
||||
),
|
||||
),
|
||||
)
|
||||
BadgeGroup(modifier = Modifier.padding(4.dp)) {
|
||||
Badge(text = "${chapterInfo.chapterCount}")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = sourcesString,
|
||||
modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
|
||||
val formattedLatestChapter by produceState(initialValue = "") {
|
||||
value = withIOContext {
|
||||
chapterInfo.getFormattedLatestChapter(context)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = formattedLatestChapter,
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp, start = 8.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import mihon.feature.migration.list.models.MigratingManga
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun MigrationItemResult(
|
||||
modifier: Modifier,
|
||||
migrationItem: MigratingManga,
|
||||
result: MigratingManga.SearchResult,
|
||||
getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?,
|
||||
getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo,
|
||||
getSourceName: (Manga) -> String,
|
||||
onMigrationItemClick: (Manga) -> Unit,
|
||||
) {
|
||||
Box(modifier.height(IntrinsicSize.Min)) {
|
||||
when (result) {
|
||||
MigratingManga.SearchResult.Searching -> Box(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxSize()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
MigratingManga.SearchResult.NotFound -> Column(
|
||||
Modifier
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxSize()
|
||||
.padding(top = 4.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio)
|
||||
.clip(MaterialTheme.shapes.extraSmall),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(SYMR.strings.no_alternatives_found),
|
||||
modifier = Modifier.padding(top = 4.dp, bottom = 1.dp, start = 8.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
is MigratingManga.SearchResult.Result -> {
|
||||
val item by produceState<Triple<Manga, MigratingManga.ChapterInfo, String>?>(
|
||||
initialValue = null,
|
||||
migrationItem,
|
||||
result,
|
||||
) {
|
||||
value = withIOContext {
|
||||
val manga = getManga(result) ?: return@withIOContext null
|
||||
Triple(
|
||||
manga,
|
||||
getChapterInfo(result),
|
||||
getSourceName(manga),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (item != null) {
|
||||
val (manga, chapterInfo, source) = item!!
|
||||
MigrationItem(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
manga = manga,
|
||||
sourcesString = source,
|
||||
chapterInfo = chapterInfo,
|
||||
onClick = {
|
||||
onMigrationItemClick(manga)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,18 +42,18 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
|||
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||
onToggleResults = screenModel::toggleFilterResults,
|
||||
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
|
||||
onClickItem = { manga ->
|
||||
onClickItem = {
|
||||
// KMK -->
|
||||
if (bulkFavoriteState.selectionMode) {
|
||||
bulkFavoriteScreenModel.toggleSelection(manga)
|
||||
bulkFavoriteScreenModel.toggleSelection(it)
|
||||
} else {
|
||||
// KMK <--
|
||||
// SY -->
|
||||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
.last()
|
||||
.newSelectedItem = mangaId to manga.id
|
||||
navigator.popUntil { it is MigrationListScreen }
|
||||
.matchOverride = mangaId to it.id
|
||||
navigator.popUntil { screen -> screen is MigrationListScreen }
|
||||
// SY <--
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
|
|
@ -15,7 +23,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.core.util.ifSourcesLoaded
|
||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
||||
import eu.kanade.presentation.browse.components.BulkFavoriteDialogs
|
||||
import eu.kanade.presentation.browse.components.bulkSelectionButton
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
|
|
@ -34,6 +41,7 @@ import mihon.feature.migration.list.MigrationListScreen
|
|||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
|
@ -121,12 +129,15 @@ data class MigrateSourceSearchScreen(
|
|||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
// SY -->
|
||||
BrowseSourceFloatingActionButton(
|
||||
isVisible = state.filters.isNotEmpty(),
|
||||
onFabClick = screenModel::openFilterSheet,
|
||||
SmallExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||
onClick = screenModel::openFilterSheet,
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = state.filters.isNotEmpty(),
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
)
|
||||
// SY <--
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
|
|
@ -135,7 +146,7 @@ data class MigrateSourceSearchScreen(
|
|||
navigator.items
|
||||
.filterIsInstance<MigrationListScreen>()
|
||||
.last()
|
||||
.newSelectedItem = currentManga.id to it.id
|
||||
.matchOverride = currentManga.id to it.id
|
||||
navigator.popUntil { it is MigrationListScreen }
|
||||
// SY <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class SmartLibrarySearchEngine(
|
|||
override fun getTitle(result: LibraryManga) = result.manga.ogTitle
|
||||
|
||||
suspend fun smartSearch(library: List<LibraryManga>, title: String): LibraryManga? =
|
||||
smartSearch(
|
||||
deepSearch(
|
||||
{ query ->
|
||||
library.filter { it.manga.ogTitle.contains(query, true) }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SmartSearchScreenModel(
|
|||
init {
|
||||
screenModelScope.launchIO {
|
||||
val result = try {
|
||||
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
||||
val resultManga = smartSearchEngine.deepSearch(source, config.origTitle)
|
||||
if (resultManga != null) {
|
||||
val localManga = networkToLocalManga(resultManga)
|
||||
SearchResults.Found(localManga)
|
||||
|
|
|
|||
|
|
@ -62,8 +62,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.feature.migration.list.MigrationListScreen
|
||||
import mihon.feature.migration.list.MigrationProcedureConfig
|
||||
import mihon.feature.migration.list.MigrationType
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.ReorderableLazyListState
|
||||
|
|
@ -100,13 +98,14 @@ class MigrationConfigScreen(private val mangaIds: List<Long>) : Screen() {
|
|||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { ScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
// KMK -->
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
BackHandler(enabled = searchQuery.isNotBlank()) {
|
||||
searchQuery = ""
|
||||
}
|
||||
// KMK <--
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
var migrationSheetOpen by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -119,9 +118,7 @@ class MigrationConfigScreen(private val mangaIds: List<Long>) : Screen() {
|
|||
navigator.replace(
|
||||
// KMK -->
|
||||
// MigrateSearchScreen(mangaId)
|
||||
MigrationListScreen(
|
||||
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), extraSearchQuery),
|
||||
),
|
||||
MigrationListScreen(mangaIds, extraSearchQuery),
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,48 +29,33 @@ import tachiyomi.i18n.sy.SYMR
|
|||
/**
|
||||
* Screen showing a list of pair of current-target manga entries being migrated.
|
||||
*/
|
||||
class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen() {
|
||||
class MigrationListScreen(private val mangaIds: List<Long>, private val extraSearchQuery: String?) : Screen() {
|
||||
|
||||
var newSelectedItem: Pair<Long, Long>? = null
|
||||
var matchOverride: Pair<Long, Long>? = null
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val screenModel = rememberScreenModel { MigrationListScreenModel(config) }
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val screenModel = rememberScreenModel { MigrationListScreenModel(mangaIds, extraSearchQuery) }
|
||||
val context = LocalContext.current
|
||||
val items by screenModel.migratingItems.collectAsState()
|
||||
val migrationDone by screenModel.migrationDone.collectAsState()
|
||||
val migrationComplete by screenModel.migrationDone.collectAsState()
|
||||
val finishedCount by screenModel.finishedCount.collectAsState()
|
||||
val dialog by screenModel.dialog.collectAsState()
|
||||
val migrateProgress by screenModel.migratingProgress.collectAsState()
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(items) {
|
||||
if (items?.isEmpty() == true) {
|
||||
val manualMigrations = screenModel.manualMigrations.value
|
||||
context.toast(
|
||||
context.pluralStringResource(
|
||||
SYMR.plurals.entry_migrated,
|
||||
manualMigrations,
|
||||
manualMigrations,
|
||||
),
|
||||
)
|
||||
if (!screenModel.hideNotFound) {
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(newSelectedItem) {
|
||||
if (newSelectedItem != null) {
|
||||
val (oldId, newId) = newSelectedItem!!
|
||||
LaunchedEffect(matchOverride) {
|
||||
if (matchOverride != null) {
|
||||
val (oldId, newId) = matchOverride!!
|
||||
screenModel.useMangaForMigration(context, newId, oldId)
|
||||
newSelectedItem = null
|
||||
matchOverride = null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(screenModel) {
|
||||
screenModel.navigateOut.collect {
|
||||
if (items.orEmpty().size == 1 && navigator.items.any { it is MangaScreen }) {
|
||||
val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Result)?.id
|
||||
val mangaId = (items.orEmpty().firstOrNull()?.searchResult?.value as? MigratingManga.SearchResult.Success)?.id
|
||||
withUIContext {
|
||||
if (mangaId != null) {
|
||||
val newStack = navigator.items.filter {
|
||||
|
|
@ -95,28 +80,44 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(items) {
|
||||
if (items?.isEmpty() == true) {
|
||||
val manualMigrations = screenModel.manualMigrations.value
|
||||
context.toast(
|
||||
context.pluralStringResource(
|
||||
SYMR.plurals.entry_migrated,
|
||||
manualMigrations,
|
||||
manualMigrations,
|
||||
),
|
||||
)
|
||||
if (!screenModel.hideUnmatched) {
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
MigrationListScreenContent(
|
||||
items = items ?: persistentListOf(),
|
||||
migrationDone = migrationDone,
|
||||
migrationComplete = migrationComplete,
|
||||
finishedCount = finishedCount,
|
||||
getManga = screenModel::getManga,
|
||||
getChapterInfo = screenModel::getChapterInfo,
|
||||
getSourceName = screenModel::getSourceName,
|
||||
onMigrationItemClick = {
|
||||
onItemClick = {
|
||||
navigator.push(MangaScreen(it.id, true))
|
||||
},
|
||||
openMigrationDialog = screenModel::openMigrateDialog,
|
||||
skipManga = { screenModel.removeManga(it) },
|
||||
// KMK -->
|
||||
cancelManga = { screenModel.cancelManga(it) },
|
||||
navigateUp = { navigator.pop() },
|
||||
openMigrationOptionsDialog = screenModel::openMigrationOptionsDialog,
|
||||
// KMK <--
|
||||
searchManually = { migrationItem ->
|
||||
onSearchManually = { migrationItem ->
|
||||
navigator push MigrateSearchScreen(migrationItem.manga.id)
|
||||
},
|
||||
migrateNow = { screenModel.migrateManga(it, true) },
|
||||
copyNow = { screenModel.migrateManga(it, false) },
|
||||
onSkip = { screenModel.removeManga(it) },
|
||||
onMigrate = { screenModel.migrateNow(it, true) },
|
||||
onCopy = { screenModel.migrateNow(it, false) },
|
||||
openMigrationDialog = screenModel::showMigrateDialog,
|
||||
// KMK -->
|
||||
onCancel = { screenModel.cancelManga(it) },
|
||||
navigateUp = { navigator.pop() },
|
||||
openOptionsDialog = screenModel::openOptionsDialog,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
val onDismissRequest = { screenModel.dialog.value = null }
|
||||
|
|
@ -124,24 +125,24 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
|||
@Suppress("NAME_SHADOWING")
|
||||
val dialog = dialog
|
||||
) {
|
||||
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
||||
is MigrationListScreenModel.Dialog.Migrate -> {
|
||||
MigrationMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
copy = dialog.copy,
|
||||
mangaSet = dialog.mangaSet,
|
||||
mangaSkipped = dialog.mangaSkipped,
|
||||
totalCount = dialog.totalCount,
|
||||
skippedCount = dialog.skippedCount,
|
||||
copyManga = screenModel::copyMangas,
|
||||
migrateManga = screenModel::migrateMangas,
|
||||
onMigrate = screenModel::migrateMangas,
|
||||
)
|
||||
}
|
||||
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
|
||||
MigrationListScreenModel.Dialog.Exit -> {
|
||||
MigrationExitDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
exitMigration = navigator::pop,
|
||||
)
|
||||
}
|
||||
// KMK -->
|
||||
MigrationListScreenModel.Dialog.MigrationOptionsDialog -> {
|
||||
MigrationListScreenModel.Dialog.Options -> {
|
||||
MigrationConfigScreenSheet(
|
||||
preferences = screenModel.preferences,
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
|
@ -164,7 +165,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
|||
}
|
||||
|
||||
BackHandler(true) {
|
||||
screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog
|
||||
screenModel.dialog.value = MigrationListScreenModel.Dialog.Exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,63 @@
|
|||
package mihon.feature.migration.list
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.CopyAll
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
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.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.components.MigrationActionIcon
|
||||
import eu.kanade.presentation.browse.components.MigrationItem
|
||||
import eu.kanade.presentation.browse.components.MigrationItemResult
|
||||
import androidx.compose.ui.unit.sp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.feature.migration.list.models.MigratingManga
|
||||
|
|
@ -35,7 +65,9 @@ import tachiyomi.core.common.util.lang.withIOContext
|
|||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
|
@ -44,22 +76,22 @@ import tachiyomi.presentation.core.util.plus
|
|||
@Composable
|
||||
fun MigrationListScreenContent(
|
||||
items: ImmutableList<MigratingManga>,
|
||||
migrationDone: Boolean,
|
||||
migrationComplete: Boolean,
|
||||
finishedCount: Int,
|
||||
getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?,
|
||||
getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo,
|
||||
getManga: suspend (MigratingManga.SearchResult.Success) -> Manga?,
|
||||
getChapterInfo: suspend (MigratingManga.SearchResult.Success) -> MigratingManga.ChapterInfo,
|
||||
getSourceName: (Manga) -> String,
|
||||
onMigrationItemClick: (Manga) -> Unit,
|
||||
onItemClick: (Manga) -> Unit,
|
||||
onSearchManually: (MigratingManga) -> Unit,
|
||||
onSkip: (Long) -> Unit,
|
||||
onMigrate: (Long) -> Unit,
|
||||
onCopy: (Long) -> Unit,
|
||||
openMigrationDialog: (Boolean) -> Unit,
|
||||
skipManga: (Long) -> Unit,
|
||||
// KMK -->
|
||||
cancelManga: (Long) -> Unit,
|
||||
onCancel: (Long) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
openMigrationOptionsDialog: () -> Unit,
|
||||
openOptionsDialog: () -> Unit,
|
||||
// KMK <--
|
||||
searchManually: (MigratingManga) -> Unit,
|
||||
migrateNow: (Long) -> Unit,
|
||||
copyNow: (Long) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
|
|
@ -81,20 +113,20 @@ fun MigrationListScreenContent(
|
|||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_settings),
|
||||
icon = Icons.Outlined.Settings,
|
||||
onClick = openMigrationOptionsDialog,
|
||||
onClick = openOptionsDialog,
|
||||
),
|
||||
// KMK <--
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.copy),
|
||||
icon = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll,
|
||||
onClick = { openMigrationDialog(true) },
|
||||
enabled = migrationDone,
|
||||
enabled = migrationComplete,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.migrate),
|
||||
icon = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll,
|
||||
onClick = { openMigrationDialog(false) },
|
||||
enabled = migrationDone,
|
||||
enabled = migrationComplete,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -103,30 +135,27 @@ fun MigrationListScreenContent(
|
|||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
items(items, key = { "migration-list-${it.manga.id}" }) { migrationItem ->
|
||||
FastScrollLazyColumn(contentPadding = contentPadding + topSmallPaddingValues) {
|
||||
items(items, key = { "migration-list-${it.manga.id}" }) { item ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.animateItemFastScroll()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val result by migrationItem.searchResult.collectAsState()
|
||||
MigrationItem(
|
||||
MigrationListItem(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.weight(1f)
|
||||
.align(Alignment.Top)
|
||||
.fillMaxHeight(),
|
||||
manga = migrationItem.manga,
|
||||
sourcesString = migrationItem.sourcesString,
|
||||
chapterInfo = migrationItem.chapterInfo,
|
||||
onClick = { onMigrationItemClick(migrationItem.manga) },
|
||||
manga = item.manga,
|
||||
source = item.source,
|
||||
chapterInfo = item.chapterInfo,
|
||||
onClick = { onItemClick(item.manga) },
|
||||
)
|
||||
|
||||
Icon(
|
||||
|
|
@ -135,34 +164,190 @@ fun MigrationListScreenContent(
|
|||
modifier = Modifier.weight(0.2f),
|
||||
)
|
||||
|
||||
MigrationItemResult(
|
||||
val result by item.searchResult.collectAsState()
|
||||
MigrationListItemResult(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.weight(1f)
|
||||
.align(Alignment.Top)
|
||||
.fillMaxHeight(),
|
||||
migrationItem = migrationItem,
|
||||
migrationItem = item,
|
||||
result = result,
|
||||
getManga = getManga,
|
||||
getChapterInfo = getChapterInfo,
|
||||
getSourceName = getSourceName,
|
||||
onMigrationItemClick = onMigrationItemClick,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
|
||||
MigrationActionIcon(
|
||||
MigrationListItemAction(
|
||||
modifier = Modifier
|
||||
.weight(0.2f),
|
||||
result = result,
|
||||
skipManga = { skipManga(migrationItem.manga.id) },
|
||||
onSearchManually = { onSearchManually(item) },
|
||||
onSkip = { onSkip(item.manga.id) },
|
||||
onMigrate = { onMigrate(item.manga.id) },
|
||||
onCopy = { onCopy(item.manga.id) },
|
||||
// KMK -->
|
||||
cancelManga = { cancelManga(migrationItem.manga.id) },
|
||||
onCancel = { onCancel(item.manga.id) },
|
||||
// KMK <--
|
||||
searchManually = { searchManually(migrationItem) },
|
||||
migrateNow = {
|
||||
migrateNow(migrationItem.manga.id)
|
||||
},
|
||||
copyNow = {
|
||||
copyNow(migrationItem.manga.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrationListItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
source: String,
|
||||
chapterInfo: MigratingManga.ChapterInfo,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(4.dp),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
Modifier.Companion.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth(),
|
||||
data = manga,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.Companion
|
||||
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||
.background(
|
||||
Brush.Companion.verticalGradient(
|
||||
0f to Color.Companion.Transparent,
|
||||
1f to Color(0xAA000000),
|
||||
),
|
||||
)
|
||||
.fillMaxHeight(0.33f)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.Companion.BottomCenter),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.Companion
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Companion.BottomStart),
|
||||
text = manga.title.ifBlank { stringResource(MR.strings.unknown) },
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 18.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Companion.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
color = Color.Companion.White,
|
||||
shadow = Shadow(
|
||||
color = Color.Companion.Black,
|
||||
blurRadius = 4f,
|
||||
),
|
||||
),
|
||||
)
|
||||
BadgeGroup(modifier = Modifier.Companion.padding(4.dp)) {
|
||||
Badge(text = "${chapterInfo.chapterCount}")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = source,
|
||||
modifier = Modifier.Companion.padding(top = 4.dp, bottom = 1.dp, start = 8.dp),
|
||||
overflow = TextOverflow.Companion.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
|
||||
val formattedLatestChapter by produceState(initialValue = "") {
|
||||
value = withIOContext {
|
||||
chapterInfo.getFormattedLatestChapter(context)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = formattedLatestChapter,
|
||||
modifier = Modifier.Companion.padding(top = 1.dp, bottom = 4.dp, start = 8.dp),
|
||||
overflow = TextOverflow.Companion.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrationListItemResult(
|
||||
modifier: Modifier,
|
||||
migrationItem: MigratingManga,
|
||||
result: MigratingManga.SearchResult,
|
||||
getManga: suspend (MigratingManga.SearchResult.Success) -> Manga?,
|
||||
getChapterInfo: suspend (MigratingManga.SearchResult.Success) -> MigratingManga.ChapterInfo,
|
||||
getSourceName: (Manga) -> String,
|
||||
onItemClick: (Manga) -> Unit,
|
||||
) {
|
||||
Box(modifier.height(IntrinsicSize.Min)) {
|
||||
when (result) {
|
||||
MigratingManga.SearchResult.Searching -> Box(
|
||||
modifier = Modifier.Companion
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxSize()
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
contentAlignment = Alignment.Companion.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
MigratingManga.SearchResult.NotFound -> Column(
|
||||
Modifier.Companion
|
||||
.widthIn(max = 150.dp)
|
||||
.fillMaxSize()
|
||||
.padding(top = 4.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(MangaCover.Book.ratio)
|
||||
.clip(MaterialTheme.shapes.extraSmall),
|
||||
contentScale = ContentScale.Companion.Crop,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(SYMR.strings.no_alternatives_found),
|
||||
modifier = Modifier.Companion.padding(top = 4.dp, bottom = 1.dp, start = 8.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
|
||||
is MigratingManga.SearchResult.Success -> {
|
||||
val item by produceState<Triple<Manga, MigratingManga.ChapterInfo, String>?>(
|
||||
initialValue = null,
|
||||
migrationItem,
|
||||
result,
|
||||
) {
|
||||
value = withIOContext {
|
||||
val manga = getManga(result) ?: return@withIOContext null
|
||||
Triple(
|
||||
manga,
|
||||
getChapterInfo(result),
|
||||
getSourceName(manga),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (item != null) {
|
||||
val (manga, chapterInfo, source) = item!!
|
||||
MigrationListItem(
|
||||
modifier = Modifier.Companion.fillMaxSize(),
|
||||
manga = manga,
|
||||
source = source,
|
||||
chapterInfo = chapterInfo,
|
||||
onClick = {
|
||||
onItemClick(manga)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -170,3 +355,77 @@ fun MigrationListScreenContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrationListItemAction(
|
||||
modifier: Modifier,
|
||||
result: MigratingManga.SearchResult,
|
||||
onSearchManually: () -> Unit,
|
||||
onSkip: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
// KMK -->
|
||||
onCancel: () -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
var moreExpanded by remember { mutableStateOf(false) }
|
||||
val closeMenu = { moreExpanded = false }
|
||||
Box(modifier) {
|
||||
when (result) {
|
||||
MigratingManga.SearchResult.Searching -> {
|
||||
// KMK -->
|
||||
IconButton(onClick = onCancel) {
|
||||
// KMK <--
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(SYMR.strings.action_stop),
|
||||
)
|
||||
}
|
||||
}
|
||||
MigratingManga.SearchResult.NotFound, is MigratingManga.SearchResult.Success -> {
|
||||
IconButton(onClick = { moreExpanded = !moreExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(MR.strings.action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = moreExpanded,
|
||||
onDismissRequest = closeMenu,
|
||||
offset = DpOffset(8.dp, (-56).dp),
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_search_manually)) },
|
||||
onClick = {
|
||||
onSearchManually()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_skip_entry)) },
|
||||
onClick = {
|
||||
onSkip()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
if (result is MigratingManga.SearchResult.Success) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_migrate_now)) },
|
||||
onClick = {
|
||||
onMigrate()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(SYMR.strings.action_copy_now)) },
|
||||
onClick = {
|
||||
onCopy()
|
||||
closeMenu()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ import uy.kohesive.injekt.api.get
|
|||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class MigrationListScreenModel(
|
||||
private val config: MigrationProcedureConfig,
|
||||
private var mangaIds: List<Long>,
|
||||
extraSearchQuery: String?,
|
||||
val preferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
|
|
@ -57,12 +58,16 @@ class MigrationListScreenModel(
|
|||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
||||
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
|
||||
// SY -->
|
||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
||||
// SY <--
|
||||
) : ScreenModel {
|
||||
|
||||
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
|
||||
private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery)
|
||||
// SY -->
|
||||
private val throttleManager = ThrottleManager()
|
||||
// SY <--
|
||||
|
||||
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
|
||||
val migrationDone = MutableStateFlow(false)
|
||||
|
|
@ -70,10 +75,10 @@ class MigrationListScreenModel(
|
|||
|
||||
val manualMigrations = MutableStateFlow(0)
|
||||
|
||||
var hideNotFound = preferences.migrationHideUnmatched().get()
|
||||
private var showOnlyUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||
private var useSourceWithMost = preferences.migrationPrioritizeByChapters().get()
|
||||
private var useSmartSearch = preferences.migrationDeepSearchMode().get()
|
||||
var hideUnmatched = preferences.migrationHideUnmatched().get()
|
||||
private var hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||
private var prioritizeByChapters = preferences.migrationPrioritizeByChapters().get()
|
||||
private var deepSearchMode = preferences.migrationDeepSearchMode().get()
|
||||
|
||||
val navigateOut = MutableSharedFlow<Unit>()
|
||||
|
||||
|
|
@ -85,12 +90,6 @@ class MigrationListScreenModel(
|
|||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
val mangaIds = when (val migration = config.migration) {
|
||||
is MigrationType.MangaList -> {
|
||||
migration.mangaIds
|
||||
}
|
||||
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
|
||||
}
|
||||
runMigrations(
|
||||
mangaIds
|
||||
.map {
|
||||
|
|
@ -99,7 +98,7 @@ class MigrationListScreenModel(
|
|||
MigratingManga(
|
||||
manga = manga,
|
||||
chapterInfo = getChapterInfo(it),
|
||||
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
||||
source = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
getMergedReferencesById.await(manga.id)
|
||||
.map { sourceManager.getOrStub(it.mangaSourceId) }
|
||||
|
|
@ -120,9 +119,9 @@ class MigrationListScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getManga(result: SearchResult.Result) = getManga(result.id)
|
||||
suspend fun getManga(result: SearchResult.Success) = getManga(result.id)
|
||||
suspend fun getManga(id: Long) = getManga.await(id)
|
||||
suspend fun getChapterInfo(result: SearchResult.Result) = getChapterInfo(result.id)
|
||||
suspend fun getChapterInfo(result: SearchResult.Success) = getChapterInfo(result.id)
|
||||
private suspend fun getChapterInfo(id: Long) = getChaptersByMangaId.await(id).let { chapters ->
|
||||
MigratingManga.ChapterInfo(
|
||||
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
|
||||
|
|
@ -145,11 +144,8 @@ class MigrationListScreenModel(
|
|||
break
|
||||
}
|
||||
// in case it was removed
|
||||
when (val migration = config.migration) {
|
||||
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
|
||||
continue
|
||||
}
|
||||
else -> Unit
|
||||
if (manga.manga.id !in mangaIds) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
||||
|
|
@ -165,107 +161,27 @@ class MigrationListScreenModel(
|
|||
} else {
|
||||
sources.filter { it.id != mangaSource.id }
|
||||
}
|
||||
when (val migration = config.migration) {
|
||||
is MigrationType.MangaSingle -> if (migration.toManga != null) {
|
||||
val localManga = getManga.await(migration.toManga)
|
||||
if (localManga != null) {
|
||||
val source = sourceManager.get(localManga.source) as? CatalogueSource
|
||||
if (source != null) {
|
||||
try {
|
||||
val chapters = if (source is EHentai) {
|
||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||
} else {
|
||||
source.getChapterList(localManga.toSManga())
|
||||
}
|
||||
syncChaptersWithSource.await(chapters, localManga, source)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
manga.progress.value = validSources.size to validSources.size
|
||||
return@async localManga
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
if (useSourceWithMost) {
|
||||
if (prioritizeByChapters) {
|
||||
val sourceSemaphore = Semaphore(3)
|
||||
val processedSources = AtomicInteger()
|
||||
|
||||
validSources.map { source ->
|
||||
async async2@{
|
||||
sourceSemaphore.withPermit {
|
||||
try {
|
||||
val searchResult = if (useSmartSearch) {
|
||||
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
|
||||
}
|
||||
|
||||
if (searchResult != null &&
|
||||
!(searchResult.url == mangaObj.url && source.id == mangaObj.source)
|
||||
) {
|
||||
val localManga = networkToLocalManga(searchResult)
|
||||
|
||||
val chapters = if (source is EHentai) {
|
||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||
} else {
|
||||
source.getChapterList(localManga.toSManga())
|
||||
}
|
||||
|
||||
try {
|
||||
syncChaptersWithSource.await(chapters, localManga, source)
|
||||
} catch (_: Exception) {
|
||||
return@async2 null
|
||||
}
|
||||
manga.progress.value =
|
||||
validSources.size to processedSources.incrementAndGet()
|
||||
localManga to chapters.size
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
val result = searchSource(manga.manga, source, deepSearchMode)
|
||||
if (result != null) {
|
||||
manga.progress.value =
|
||||
validSources.size to processedSources.incrementAndGet()
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}.mapNotNull { it.await() }.maxByOrNull { it.second }?.first
|
||||
} else {
|
||||
validSources.forEachIndexed { index, source ->
|
||||
val searchResult = try {
|
||||
val searchResult = if (useSmartSearch) {
|
||||
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
|
||||
} else {
|
||||
smartSearchEngine.normalSearch(source, mangaObj.ogTitle)
|
||||
}
|
||||
|
||||
if (searchResult != null) {
|
||||
val localManga = networkToLocalManga(searchResult)
|
||||
val chapters = try {
|
||||
if (source is EHentai) {
|
||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||
} else {
|
||||
source.getChapterList(localManga.toSManga())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
|
||||
emptyList()
|
||||
}
|
||||
syncChaptersWithSource.await(chapters, localManga, source)
|
||||
localManga
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
val result = searchSource(manga.manga, source, deepSearchMode)
|
||||
manga.progress.value = validSources.size to (index + 1)
|
||||
if (searchResult != null) return@async searchResult
|
||||
if (result != null) return@async result.first
|
||||
}
|
||||
|
||||
null
|
||||
|
|
@ -294,37 +210,82 @@ class MigrationListScreenModel(
|
|||
manga.searchResult.value = if (result == null) {
|
||||
SearchResult.NotFound
|
||||
} else {
|
||||
SearchResult.Result(result.id)
|
||||
SearchResult.Success(result.id)
|
||||
}
|
||||
if (result == null && hideNotFound) {
|
||||
if (result == null && hideUnmatched) {
|
||||
removeManga(manga)
|
||||
}
|
||||
if (result != null &&
|
||||
showOnlyUpdates &&
|
||||
hideWithoutUpdates &&
|
||||
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
||||
) {
|
||||
removeManga(manga)
|
||||
}
|
||||
|
||||
sourceFinished()
|
||||
updateMigrationProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sourceFinished() {
|
||||
finishedCount.value = migratingItems.value.orEmpty().count {
|
||||
it.searchResult.value != SearchResult.Searching
|
||||
}
|
||||
if (allMangasDone()) {
|
||||
migrationDone.value = true
|
||||
}
|
||||
if (migratingItems.value?.isEmpty() == true) {
|
||||
navigateOut()
|
||||
private suspend fun searchSource(
|
||||
manga: Manga,
|
||||
source: CatalogueSource,
|
||||
deepSearchMode: Boolean,
|
||||
): Pair<Manga, Int>? {
|
||||
return try {
|
||||
val searchResult = if (deepSearchMode) {
|
||||
smartSearchEngine.deepSearch(source, manga.ogTitle)
|
||||
} else {
|
||||
smartSearchEngine.regularSearch(source, manga.ogTitle)
|
||||
}
|
||||
|
||||
if (searchResult != null &&
|
||||
!(searchResult.url == manga.url && source.id == manga.source)
|
||||
) {
|
||||
val localManga = networkToLocalManga(searchResult)
|
||||
|
||||
val chapters = try {
|
||||
if (source is EHentai) {
|
||||
source.getChapterList(localManga.toSManga(), throttleManager::throttle)
|
||||
} else {
|
||||
source.getChapterList(localManga.toSManga())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
this@MigrationListScreenModel.logcat(LogPriority.ERROR, e)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
syncChaptersWithSource.await(chapters, localManga, source)
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
localManga to chapters.size
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
// Ignore cancellations
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun allMangasDone() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } &&
|
||||
migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Result }
|
||||
private suspend fun updateMigrationProgress() {
|
||||
finishedCount.value = migratingItems.value.orEmpty().count {
|
||||
it.searchResult.value != SearchResult.Searching
|
||||
}
|
||||
if (migrationComplete()) {
|
||||
migrationDone.value = true
|
||||
}
|
||||
if (migratingItems.value?.isEmpty() == true) {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrationComplete() = migratingItems.value.orEmpty().all { it.searchResult.value != SearchResult.Searching } &&
|
||||
migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Success }
|
||||
|
||||
private fun mangasSkipped() = migratingItems.value.orEmpty().count { it.searchResult.value == SearchResult.NotFound }
|
||||
|
||||
|
|
@ -357,7 +318,7 @@ class MigrationListScreenModel(
|
|||
Timber.tag("MigrationListScreenModel").e(e, "Error updating manga from source")
|
||||
}
|
||||
|
||||
migratingManga.searchResult.value = SearchResult.Result(result.id)
|
||||
migratingManga.searchResult.value = SearchResult.Success(result.id)
|
||||
} else {
|
||||
migratingManga.searchResult.value = SearchResult.NotFound
|
||||
withUIContext {
|
||||
|
|
@ -366,7 +327,7 @@ class MigrationListScreenModel(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
sourceFinished()
|
||||
updateMigrationProgress()
|
||||
// KMK <--
|
||||
}
|
||||
}
|
||||
|
|
@ -385,18 +346,18 @@ class MigrationListScreenModel(
|
|||
migratingProgress.value = 0f
|
||||
val items = migratingItems.value.orEmpty()
|
||||
try {
|
||||
items.forEachIndexed { index, current ->
|
||||
items.forEachIndexed { index, manga ->
|
||||
try {
|
||||
ensureActive()
|
||||
val target = current.searchResult.value.let {
|
||||
if (it is SearchResult.Result) {
|
||||
val target = manga.searchResult.value.let {
|
||||
if (it is SearchResult.Success) {
|
||||
getManga.await(it.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (target != null) {
|
||||
migrateManga(current.manga, target, replace)
|
||||
migrateManga(manga.manga, target, replace)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
|
@ -405,7 +366,7 @@ class MigrationListScreenModel(
|
|||
migratingProgress.value = index.toFloat() / items.size
|
||||
}
|
||||
|
||||
navigateOut()
|
||||
navigateBack()
|
||||
} finally {
|
||||
migratingProgress.value = Float.MAX_VALUE
|
||||
migrateJob = null
|
||||
|
|
@ -418,20 +379,20 @@ class MigrationListScreenModel(
|
|||
migrateJob = null
|
||||
}
|
||||
|
||||
private suspend fun navigateOut() {
|
||||
private suspend fun navigateBack() {
|
||||
navigateOut.emit(Unit)
|
||||
}
|
||||
|
||||
fun migrateManga(mangaId: Long, replace: Boolean) {
|
||||
fun migrateNow(mangaId: Long, replace: Boolean) {
|
||||
manualMigrations.value++
|
||||
screenModelScope.launchIO {
|
||||
val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
||||
val manga = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
||||
?: return@launchIO
|
||||
|
||||
val target = getManga.await((current.searchResult.value as? SearchResult.Result)?.id ?: return@launchIO)
|
||||
val target = getManga.await((manga.searchResult.value as? SearchResult.Success)?.id ?: return@launchIO)
|
||||
?: return@launchIO
|
||||
|
||||
migrateManga(current.manga, target, replace)
|
||||
migrateManga(manga.manga, target, replace)
|
||||
removeManga(mangaId)
|
||||
}
|
||||
}
|
||||
|
|
@ -445,7 +406,7 @@ class MigrationListScreenModel(
|
|||
item.searchingJob?.cancel()
|
||||
item.searchingJob = null
|
||||
item.searchResult.value = SearchResult.NotFound
|
||||
sourceFinished()
|
||||
updateMigrationProgress()
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
|
@ -456,23 +417,18 @@ class MigrationListScreenModel(
|
|||
?: return@launchIO
|
||||
removeManga(item)
|
||||
item.migrationScope.cancel()
|
||||
sourceFinished()
|
||||
updateMigrationProgress()
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeManga(item: MigratingManga) {
|
||||
when (val migration = config.migration) {
|
||||
is MigrationType.MangaList -> {
|
||||
val ids = migration.mangaIds.toMutableList()
|
||||
val index = ids.indexOf(item.manga.id)
|
||||
if (index > -1) {
|
||||
ids.removeAt(index)
|
||||
config.migration = MigrationType.MangaList(ids)
|
||||
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
||||
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
||||
}
|
||||
}
|
||||
is MigrationType.MangaSingle -> Unit
|
||||
val ids = mangaIds.toMutableList()
|
||||
val index = ids.indexOf(item.manga.id)
|
||||
if (index > -1) {
|
||||
ids.removeAt(index)
|
||||
mangaIds = ids
|
||||
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
||||
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -483,10 +439,8 @@ class MigrationListScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun openMigrateDialog(
|
||||
copy: Boolean,
|
||||
) {
|
||||
dialog.value = Dialog.MigrateMangaDialog(
|
||||
fun showMigrateDialog(copy: Boolean) {
|
||||
dialog.value = Dialog.Migrate(
|
||||
copy,
|
||||
migratingItems.value.orEmpty().size,
|
||||
mangasSkipped(),
|
||||
|
|
@ -494,24 +448,24 @@ class MigrationListScreenModel(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
fun openMigrationOptionsDialog() {
|
||||
dialog.value = Dialog.MigrationOptionsDialog
|
||||
fun openOptionsDialog() {
|
||||
dialog.value = Dialog.Options
|
||||
}
|
||||
|
||||
fun updateOptions() {
|
||||
hideNotFound = preferences.migrationHideUnmatched().get()
|
||||
showOnlyUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||
useSourceWithMost = preferences.migrationPrioritizeByChapters().get()
|
||||
useSmartSearch = preferences.migrationDeepSearchMode().get()
|
||||
hideUnmatched = preferences.migrationHideUnmatched().get()
|
||||
hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||
prioritizeByChapters = preferences.migrationPrioritizeByChapters().get()
|
||||
deepSearchMode = preferences.migrationDeepSearchMode().get()
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
sealed class Dialog {
|
||||
data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog()
|
||||
data object MigrationExitDialog : Dialog()
|
||||
data class Migrate(val copy: Boolean, val totalCount: Int, val skippedCount: Int) : Dialog()
|
||||
data object Exit : Dialog()
|
||||
|
||||
// KMK -->
|
||||
data object MigrationOptionsDialog : Dialog()
|
||||
data object Options : Dialog()
|
||||
// KMK <--
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
package mihon.feature.migration.list
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
sealed class MigrationType : Serializable {
|
||||
data class MangaList(val mangaIds: List<Long>) : MigrationType()
|
||||
data class MangaSingle(val fromMangaId: Long, val toManga: Long?) : MigrationType()
|
||||
}
|
||||
|
||||
data class MigrationProcedureConfig(
|
||||
var migration: MigrationType,
|
||||
val extraSearchParams: String?,
|
||||
) : Serializable
|
||||
|
|
@ -15,6 +15,9 @@ fun MigrationExitDialog(
|
|||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(SYMR.strings.stop_migrating))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = exitMigration) {
|
||||
Text(text = stringResource(SYMR.strings.action_stop))
|
||||
|
|
@ -25,8 +28,5 @@ fun MigrationExitDialog(
|
|||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(SYMR.strings.stop_migrating))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,20 +13,30 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||
fun MigrationMangaDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
copy: Boolean,
|
||||
mangaSet: Int,
|
||||
mangaSkipped: Int,
|
||||
totalCount: Int,
|
||||
skippedCount: Int,
|
||||
copyManga: () -> Unit,
|
||||
migrateManga: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
text = {
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
if (copy) SYMR.plurals.copy_entry else SYMR.plurals.migrate_entry,
|
||||
count = totalCount,
|
||||
totalCount,
|
||||
(if (skippedCount > 0) " " + stringResource(SYMR.strings.skipping_, skippedCount) else ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (copy) {
|
||||
copyManga()
|
||||
} else {
|
||||
migrateManga()
|
||||
onMigrate()
|
||||
}
|
||||
},
|
||||
) {
|
||||
|
|
@ -38,15 +48,5 @@ fun MigrationMangaDialog(
|
|||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
if (copy) SYMR.plurals.copy_entry else SYMR.plurals.migrate_entry,
|
||||
count = mangaSet,
|
||||
mangaSet,
|
||||
(if (mangaSkipped > 0) " " + stringResource(SYMR.strings.skipping_, mangaSkipped) else ""),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import kotlin.coroutines.CoroutineContext
|
|||
class MigratingManga(
|
||||
val manga: Manga,
|
||||
val chapterInfo: ChapterInfo,
|
||||
val sourcesString: String,
|
||||
val source: String,
|
||||
parentContext: CoroutineContext,
|
||||
) {
|
||||
val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default)
|
||||
|
|
@ -33,7 +33,7 @@ class MigratingManga(
|
|||
sealed class SearchResult {
|
||||
data object Searching : SearchResult()
|
||||
data object NotFound : SearchResult()
|
||||
data class Result(val id: Long) : SearchResult()
|
||||
data class Success(val id: Long) : SearchResult()
|
||||
}
|
||||
|
||||
data class ChapterInfo(
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
|
||||
protected abstract fun getTitle(result: T): String
|
||||
|
||||
protected suspend fun smartSearch(searchAction: SearchAction<T>, title: String): T? {
|
||||
protected suspend fun deepSearch(searchAction: SearchAction<T>, title: String): T? {
|
||||
val cleanedTitle = cleanSmartSearchTitle(title)
|
||||
|
||||
val queries = getSmartSearchQueries(cleanedTitle)
|
||||
val queries = getDeepSearchQueries(cleanedTitle)
|
||||
|
||||
val eligibleManga = supervisorScope {
|
||||
queries.map { query ->
|
||||
|
|
@ -42,10 +42,10 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
}.flatMap { it.await() }
|
||||
}
|
||||
|
||||
return eligibleManga.maxByOrNull { it.dist }?.manga
|
||||
return eligibleManga.maxByOrNull { it.distance }?.entry
|
||||
}
|
||||
|
||||
protected suspend fun normalSearch(searchAction: SearchAction<T>, title: String): T? {
|
||||
protected suspend fun regularSearch(searchAction: SearchAction<T>, title: String): T? {
|
||||
val eligibleManga = supervisorScope {
|
||||
val searchQuery = if (extraSearchParams != null) {
|
||||
"$title ${extraSearchParams.trim()}"
|
||||
|
|
@ -66,7 +66,7 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
}
|
||||
}
|
||||
|
||||
return eligibleManga.maxByOrNull { it.dist }?.manga
|
||||
return eligibleManga.maxByOrNull { it.distance }?.entry
|
||||
}
|
||||
|
||||
private fun cleanSmartSearchTitle(title: String): String {
|
||||
|
|
@ -142,7 +142,7 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
return result.toString()
|
||||
}
|
||||
|
||||
private fun getSmartSearchQueries(cleanedTitle: String): List<String> {
|
||||
private fun getDeepSearchQueries(cleanedTitle: String): List<String> {
|
||||
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||
|
||||
|
|
@ -155,7 +155,6 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
// Search largest word
|
||||
// Search first two words
|
||||
// Search first word
|
||||
|
||||
val searchQueries = listOf(
|
||||
listOf(cleanedTitle),
|
||||
splitSortedByLargest.take(2),
|
||||
|
|
@ -179,4 +178,4 @@ abstract class BaseSmartSearchEngine<T>(
|
|||
}
|
||||
}
|
||||
|
||||
data class SearchEntry<T>(val manga: T, val dist: Double)
|
||||
data class SearchEntry<T>(val entry: T, val distance: Double)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ class SmartSourceSearchEngine(
|
|||
|
||||
override fun getTitle(result: SManga) = result.originalTitle
|
||||
|
||||
suspend fun smartSearch(source: CatalogueSource, title: String): Manga? =
|
||||
smartSearch(makeSearchAction(source), title).let {
|
||||
suspend fun regularSearch(source: CatalogueSource, title: String): Manga? =
|
||||
regularSearch(makeSearchAction(source), title).let {
|
||||
it?.toDomainManga(source.id)
|
||||
}
|
||||
|
||||
suspend fun normalSearch(source: CatalogueSource, title: String): Manga? =
|
||||
normalSearch(makeSearchAction(source), title).let {
|
||||
suspend fun deepSearch(source: CatalogueSource, title: String): Manga? =
|
||||
deepSearch(makeSearchAction(source), title).let {
|
||||
it?.toDomainManga(source.id)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue