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.layout.PaddingValues
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material.icons.outlined.Public
|
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.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
|
|
@ -12,7 +16,6 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import dev.icerock.moko.resources.StringResource
|
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.GlobalSearchCardRow
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
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.domain.source.model.SavedSearch
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
|
@ -156,10 +160,21 @@ fun SourceFeedScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
BrowseSourceFloatingActionButton(
|
// KMK -->
|
||||||
isVisible = hasFilters,
|
SmallExtendedFloatingActionButton(
|
||||||
onFabClick = onFabClick,
|
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 ->
|
) { paddingValues ->
|
||||||
Crossfade(targetState = isLoading, label = "source_feed") { state ->
|
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,
|
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||||
onToggleResults = screenModel::toggleFilterResults,
|
onToggleResults = screenModel::toggleFilterResults,
|
||||||
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
|
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
|
||||||
onClickItem = { manga ->
|
onClickItem = {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (bulkFavoriteState.selectionMode) {
|
if (bulkFavoriteState.selectionMode) {
|
||||||
bulkFavoriteScreenModel.toggleSelection(manga)
|
bulkFavoriteScreenModel.toggleSelection(it)
|
||||||
} else {
|
} else {
|
||||||
// KMK <--
|
// KMK <--
|
||||||
// SY -->
|
// SY -->
|
||||||
navigator.items
|
navigator.items
|
||||||
.filterIsInstance<MigrationListScreen>()
|
.filterIsInstance<MigrationListScreen>()
|
||||||
.last()
|
.last()
|
||||||
.newSelectedItem = mangaId to manga.id
|
.matchOverride = mangaId to it.id
|
||||||
navigator.popUntil { it is MigrationListScreen }
|
navigator.popUntil { screen -> screen is MigrationListScreen }
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
package eu.kanade.tachiyomi.ui.browse.migration.search
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
|
@ -15,7 +23,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
import eu.kanade.core.util.ifSourcesLoaded
|
||||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
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.BulkFavoriteDialogs
|
||||||
import eu.kanade.presentation.browse.components.bulkSelectionButton
|
import eu.kanade.presentation.browse.components.bulkSelectionButton
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
|
|
@ -34,6 +41,7 @@ import mihon.feature.migration.list.MigrationListScreen
|
||||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
@ -121,12 +129,15 @@ data class MigrateSourceSearchScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
// SY -->
|
SmallExtendedFloatingActionButton(
|
||||||
BrowseSourceFloatingActionButton(
|
text = { Text(text = stringResource(MR.strings.action_filter)) },
|
||||||
isVisible = state.filters.isNotEmpty(),
|
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
|
||||||
onFabClick = screenModel::openFilterSheet,
|
onClick = screenModel::openFilterSheet,
|
||||||
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
|
visible = state.filters.isNotEmpty(),
|
||||||
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// SY <--
|
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|
@ -135,7 +146,7 @@ data class MigrateSourceSearchScreen(
|
||||||
navigator.items
|
navigator.items
|
||||||
.filterIsInstance<MigrationListScreen>()
|
.filterIsInstance<MigrationListScreen>()
|
||||||
.last()
|
.last()
|
||||||
.newSelectedItem = currentManga.id to it.id
|
.matchOverride = currentManga.id to it.id
|
||||||
navigator.popUntil { it is MigrationListScreen }
|
navigator.popUntil { it is MigrationListScreen }
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class SmartLibrarySearchEngine(
|
||||||
override fun getTitle(result: LibraryManga) = result.manga.ogTitle
|
override fun getTitle(result: LibraryManga) = result.manga.ogTitle
|
||||||
|
|
||||||
suspend fun smartSearch(library: List<LibraryManga>, title: String): LibraryManga? =
|
suspend fun smartSearch(library: List<LibraryManga>, title: String): LibraryManga? =
|
||||||
smartSearch(
|
deepSearch(
|
||||||
{ query ->
|
{ query ->
|
||||||
library.filter { it.manga.ogTitle.contains(query, true) }
|
library.filter { it.manga.ogTitle.contains(query, true) }
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class SmartSearchScreenModel(
|
||||||
init {
|
init {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
val result = try {
|
val result = try {
|
||||||
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
val resultManga = smartSearchEngine.deepSearch(source, config.origTitle)
|
||||||
if (resultManga != null) {
|
if (resultManga != null) {
|
||||||
val localManga = networkToLocalManga(resultManga)
|
val localManga = networkToLocalManga(resultManga)
|
||||||
SearchResults.Found(localManga)
|
SearchResults.Found(localManga)
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import mihon.feature.migration.list.MigrationListScreen
|
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.ReorderableCollectionItemScope
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.ReorderableLazyListState
|
import sh.calvin.reorderable.ReorderableLazyListState
|
||||||
|
|
@ -100,13 +98,14 @@ class MigrationConfigScreen(private val mangaIds: List<Long>) : Screen() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val screenModel = rememberScreenModel { ScreenModel() }
|
val screenModel = rememberScreenModel { ScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
BackHandler(enabled = searchQuery.isNotBlank()) {
|
BackHandler(enabled = searchQuery.isNotBlank()) {
|
||||||
searchQuery = ""
|
searchQuery = ""
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val state by screenModel.state.collectAsState()
|
|
||||||
|
|
||||||
var migrationSheetOpen by rememberSaveable { mutableStateOf(false) }
|
var migrationSheetOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -119,9 +118,7 @@ class MigrationConfigScreen(private val mangaIds: List<Long>) : Screen() {
|
||||||
navigator.replace(
|
navigator.replace(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
// MigrateSearchScreen(mangaId)
|
// MigrateSearchScreen(mangaId)
|
||||||
MigrationListScreen(
|
MigrationListScreen(mangaIds, extraSearchQuery),
|
||||||
MigrationProcedureConfig(MigrationType.MangaList(mangaIds), extraSearchQuery),
|
|
||||||
),
|
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,48 +29,33 @@ import tachiyomi.i18n.sy.SYMR
|
||||||
/**
|
/**
|
||||||
* Screen showing a list of pair of current-target manga entries being migrated.
|
* 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
|
@Composable
|
||||||
override fun Content() {
|
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 items by screenModel.migratingItems.collectAsState()
|
||||||
val migrationDone by screenModel.migrationDone.collectAsState()
|
val migrationComplete by screenModel.migrationDone.collectAsState()
|
||||||
val finishedCount by screenModel.finishedCount.collectAsState()
|
val finishedCount by screenModel.finishedCount.collectAsState()
|
||||||
val dialog by screenModel.dialog.collectAsState()
|
val dialog by screenModel.dialog.collectAsState()
|
||||||
val migrateProgress by screenModel.migratingProgress.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) {
|
LaunchedEffect(matchOverride) {
|
||||||
if (newSelectedItem != null) {
|
if (matchOverride != null) {
|
||||||
val (oldId, newId) = newSelectedItem!!
|
val (oldId, newId) = matchOverride!!
|
||||||
screenModel.useMangaForMigration(context, newId, oldId)
|
screenModel.useMangaForMigration(context, newId, oldId)
|
||||||
newSelectedItem = null
|
matchOverride = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(screenModel) {
|
LaunchedEffect(screenModel) {
|
||||||
screenModel.navigateOut.collect {
|
screenModel.navigateOut.collect {
|
||||||
if (items.orEmpty().size == 1 && navigator.items.any { it is MangaScreen }) {
|
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 {
|
withUIContext {
|
||||||
if (mangaId != null) {
|
if (mangaId != null) {
|
||||||
val newStack = navigator.items.filter {
|
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(
|
MigrationListScreenContent(
|
||||||
items = items ?: persistentListOf(),
|
items = items ?: persistentListOf(),
|
||||||
migrationDone = migrationDone,
|
migrationComplete = migrationComplete,
|
||||||
finishedCount = finishedCount,
|
finishedCount = finishedCount,
|
||||||
getManga = screenModel::getManga,
|
getManga = screenModel::getManga,
|
||||||
getChapterInfo = screenModel::getChapterInfo,
|
getChapterInfo = screenModel::getChapterInfo,
|
||||||
getSourceName = screenModel::getSourceName,
|
getSourceName = screenModel::getSourceName,
|
||||||
onMigrationItemClick = {
|
onItemClick = {
|
||||||
navigator.push(MangaScreen(it.id, true))
|
navigator.push(MangaScreen(it.id, true))
|
||||||
},
|
},
|
||||||
openMigrationDialog = screenModel::openMigrateDialog,
|
onSearchManually = { migrationItem ->
|
||||||
skipManga = { screenModel.removeManga(it) },
|
|
||||||
// KMK -->
|
|
||||||
cancelManga = { screenModel.cancelManga(it) },
|
|
||||||
navigateUp = { navigator.pop() },
|
|
||||||
openMigrationOptionsDialog = screenModel::openMigrationOptionsDialog,
|
|
||||||
// KMK <--
|
|
||||||
searchManually = { migrationItem ->
|
|
||||||
navigator push MigrateSearchScreen(migrationItem.manga.id)
|
navigator push MigrateSearchScreen(migrationItem.manga.id)
|
||||||
},
|
},
|
||||||
migrateNow = { screenModel.migrateManga(it, true) },
|
onSkip = { screenModel.removeManga(it) },
|
||||||
copyNow = { screenModel.migrateManga(it, false) },
|
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 }
|
val onDismissRequest = { screenModel.dialog.value = null }
|
||||||
|
|
@ -124,24 +125,24 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
val dialog = dialog
|
val dialog = dialog
|
||||||
) {
|
) {
|
||||||
is MigrationListScreenModel.Dialog.MigrateMangaDialog -> {
|
is MigrationListScreenModel.Dialog.Migrate -> {
|
||||||
MigrationMangaDialog(
|
MigrationMangaDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
copy = dialog.copy,
|
copy = dialog.copy,
|
||||||
mangaSet = dialog.mangaSet,
|
totalCount = dialog.totalCount,
|
||||||
mangaSkipped = dialog.mangaSkipped,
|
skippedCount = dialog.skippedCount,
|
||||||
copyManga = screenModel::copyMangas,
|
copyManga = screenModel::copyMangas,
|
||||||
migrateManga = screenModel::migrateMangas,
|
onMigrate = screenModel::migrateMangas,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MigrationListScreenModel.Dialog.MigrationExitDialog -> {
|
MigrationListScreenModel.Dialog.Exit -> {
|
||||||
MigrationExitDialog(
|
MigrationExitDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
exitMigration = navigator::pop,
|
exitMigration = navigator::pop,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// KMK -->
|
// KMK -->
|
||||||
MigrationListScreenModel.Dialog.MigrationOptionsDialog -> {
|
MigrationListScreenModel.Dialog.Options -> {
|
||||||
MigrationConfigScreenSheet(
|
MigrationConfigScreenSheet(
|
||||||
preferences = screenModel.preferences,
|
preferences = screenModel.preferences,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|
@ -164,7 +165,7 @@ class MigrationListScreen(private val config: MigrationProcedureConfig) : Screen
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(true) {
|
BackHandler(true) {
|
||||||
screenModel.dialog.value = MigrationListScreenModel.Dialog.MigrationExitDialog
|
screenModel.dialog.value = MigrationListScreenModel.Dialog.Exit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,63 @@
|
||||||
package mihon.feature.migration.list
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
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.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.CopyAll
|
import androidx.compose.material.icons.outlined.CopyAll
|
||||||
import androidx.compose.material.icons.outlined.Done
|
import androidx.compose.material.icons.outlined.Done
|
||||||
import androidx.compose.material.icons.outlined.DoneAll
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
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.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.Composable
|
||||||
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.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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 androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.browse.components.MigrationActionIcon
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.browse.components.MigrationItem
|
|
||||||
import eu.kanade.presentation.browse.components.MigrationItemResult
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import mihon.feature.migration.list.models.MigratingManga
|
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.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
@ -44,22 +76,22 @@ import tachiyomi.presentation.core.util.plus
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrationListScreenContent(
|
fun MigrationListScreenContent(
|
||||||
items: ImmutableList<MigratingManga>,
|
items: ImmutableList<MigratingManga>,
|
||||||
migrationDone: Boolean,
|
migrationComplete: Boolean,
|
||||||
finishedCount: Int,
|
finishedCount: Int,
|
||||||
getManga: suspend (MigratingManga.SearchResult.Result) -> Manga?,
|
getManga: suspend (MigratingManga.SearchResult.Success) -> Manga?,
|
||||||
getChapterInfo: suspend (MigratingManga.SearchResult.Result) -> MigratingManga.ChapterInfo,
|
getChapterInfo: suspend (MigratingManga.SearchResult.Success) -> MigratingManga.ChapterInfo,
|
||||||
getSourceName: (Manga) -> String,
|
getSourceName: (Manga) -> String,
|
||||||
onMigrationItemClick: (Manga) -> Unit,
|
onItemClick: (Manga) -> Unit,
|
||||||
|
onSearchManually: (MigratingManga) -> Unit,
|
||||||
|
onSkip: (Long) -> Unit,
|
||||||
|
onMigrate: (Long) -> Unit,
|
||||||
|
onCopy: (Long) -> Unit,
|
||||||
openMigrationDialog: (Boolean) -> Unit,
|
openMigrationDialog: (Boolean) -> Unit,
|
||||||
skipManga: (Long) -> Unit,
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
cancelManga: (Long) -> Unit,
|
onCancel: (Long) -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
openMigrationOptionsDialog: () -> Unit,
|
openOptionsDialog: () -> Unit,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
searchManually: (MigratingManga) -> Unit,
|
|
||||||
migrateNow: (Long) -> Unit,
|
|
||||||
copyNow: (Long) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
|
|
@ -81,20 +113,20 @@ fun MigrationListScreenContent(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_settings),
|
title = stringResource(MR.strings.action_settings),
|
||||||
icon = Icons.Outlined.Settings,
|
icon = Icons.Outlined.Settings,
|
||||||
onClick = openMigrationOptionsDialog,
|
onClick = openOptionsDialog,
|
||||||
),
|
),
|
||||||
// KMK <--
|
// KMK <--
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.copy),
|
title = stringResource(MR.strings.copy),
|
||||||
icon = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll,
|
icon = if (items.size == 1) Icons.Outlined.ContentCopy else Icons.Outlined.CopyAll,
|
||||||
onClick = { openMigrationDialog(true) },
|
onClick = { openMigrationDialog(true) },
|
||||||
enabled = migrationDone,
|
enabled = migrationComplete,
|
||||||
),
|
),
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.migrate),
|
title = stringResource(MR.strings.migrate),
|
||||||
icon = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll,
|
icon = if (items.size == 1) Icons.Outlined.Done else Icons.Outlined.DoneAll,
|
||||||
onClick = { openMigrationDialog(false) },
|
onClick = { openMigrationDialog(false) },
|
||||||
enabled = migrationDone,
|
enabled = migrationComplete,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -103,30 +135,27 @@ fun MigrationListScreenContent(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
ScrollbarLazyColumn(
|
FastScrollLazyColumn(contentPadding = contentPadding + topSmallPaddingValues) {
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
items(items, key = { "migration-list-${it.manga.id}" }) { item ->
|
||||||
) {
|
|
||||||
items(items, key = { "migration-list-${it.manga.id}" }) { migrationItem ->
|
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateItem()
|
.animateItemFastScroll()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
val result by migrationItem.searchResult.collectAsState()
|
MigrationListItem(
|
||||||
MigrationItem(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.align(Alignment.Top)
|
.align(Alignment.Top)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
manga = migrationItem.manga,
|
manga = item.manga,
|
||||||
sourcesString = migrationItem.sourcesString,
|
source = item.source,
|
||||||
chapterInfo = migrationItem.chapterInfo,
|
chapterInfo = item.chapterInfo,
|
||||||
onClick = { onMigrationItemClick(migrationItem.manga) },
|
onClick = { onItemClick(item.manga) },
|
||||||
)
|
)
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -135,34 +164,190 @@ fun MigrationListScreenContent(
|
||||||
modifier = Modifier.weight(0.2f),
|
modifier = Modifier.weight(0.2f),
|
||||||
)
|
)
|
||||||
|
|
||||||
MigrationItemResult(
|
val result by item.searchResult.collectAsState()
|
||||||
|
MigrationListItemResult(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.align(Alignment.Top)
|
.align(Alignment.Top)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
migrationItem = migrationItem,
|
migrationItem = item,
|
||||||
result = result,
|
result = result,
|
||||||
getManga = getManga,
|
getManga = getManga,
|
||||||
getChapterInfo = getChapterInfo,
|
getChapterInfo = getChapterInfo,
|
||||||
getSourceName = getSourceName,
|
getSourceName = getSourceName,
|
||||||
onMigrationItemClick = onMigrationItemClick,
|
onItemClick = onItemClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
MigrationActionIcon(
|
MigrationListItemAction(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(0.2f),
|
.weight(0.2f),
|
||||||
result = result,
|
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 -->
|
// KMK -->
|
||||||
cancelManga = { cancelManga(migrationItem.manga.id) },
|
onCancel = { onCancel(item.manga.id) },
|
||||||
// KMK <--
|
// 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
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class MigrationListScreenModel(
|
class MigrationListScreenModel(
|
||||||
private val config: MigrationProcedureConfig,
|
private var mangaIds: List<Long>,
|
||||||
|
extraSearchQuery: String?,
|
||||||
val preferences: SourcePreferences = Injekt.get(),
|
val preferences: SourcePreferences = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
|
|
@ -57,12 +58,16 @@ class MigrationListScreenModel(
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
|
||||||
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
|
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
|
||||||
|
// SY -->
|
||||||
|
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
||||||
|
// SY <--
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
private val smartSearchEngine = SmartSourceSearchEngine(config.extraSearchParams)
|
private val smartSearchEngine = SmartSourceSearchEngine(extraSearchQuery)
|
||||||
|
// SY -->
|
||||||
private val throttleManager = ThrottleManager()
|
private val throttleManager = ThrottleManager()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
|
val migratingItems = MutableStateFlow<ImmutableList<MigratingManga>?>(null)
|
||||||
val migrationDone = MutableStateFlow(false)
|
val migrationDone = MutableStateFlow(false)
|
||||||
|
|
@ -70,10 +75,10 @@ class MigrationListScreenModel(
|
||||||
|
|
||||||
val manualMigrations = MutableStateFlow(0)
|
val manualMigrations = MutableStateFlow(0)
|
||||||
|
|
||||||
var hideNotFound = preferences.migrationHideUnmatched().get()
|
var hideUnmatched = preferences.migrationHideUnmatched().get()
|
||||||
private var showOnlyUpdates = preferences.migrationHideWithoutUpdates().get()
|
private var hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||||
private var useSourceWithMost = preferences.migrationPrioritizeByChapters().get()
|
private var prioritizeByChapters = preferences.migrationPrioritizeByChapters().get()
|
||||||
private var useSmartSearch = preferences.migrationDeepSearchMode().get()
|
private var deepSearchMode = preferences.migrationDeepSearchMode().get()
|
||||||
|
|
||||||
val navigateOut = MutableSharedFlow<Unit>()
|
val navigateOut = MutableSharedFlow<Unit>()
|
||||||
|
|
||||||
|
|
@ -85,12 +90,6 @@ class MigrationListScreenModel(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
val mangaIds = when (val migration = config.migration) {
|
|
||||||
is MigrationType.MangaList -> {
|
|
||||||
migration.mangaIds
|
|
||||||
}
|
|
||||||
is MigrationType.MangaSingle -> listOf(migration.fromMangaId)
|
|
||||||
}
|
|
||||||
runMigrations(
|
runMigrations(
|
||||||
mangaIds
|
mangaIds
|
||||||
.map {
|
.map {
|
||||||
|
|
@ -99,7 +98,7 @@ class MigrationListScreenModel(
|
||||||
MigratingManga(
|
MigratingManga(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
chapterInfo = getChapterInfo(it),
|
chapterInfo = getChapterInfo(it),
|
||||||
sourcesString = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
source = sourceManager.getOrStub(manga.source).getNameForMangaInfo(
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
getMergedReferencesById.await(manga.id)
|
getMergedReferencesById.await(manga.id)
|
||||||
.map { sourceManager.getOrStub(it.mangaSourceId) }
|
.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 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 ->
|
private suspend fun getChapterInfo(id: Long) = getChaptersByMangaId.await(id).let { chapters ->
|
||||||
MigratingManga.ChapterInfo(
|
MigratingManga.ChapterInfo(
|
||||||
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
|
latestChapter = chapters.maxOfOrNull { it.chapterNumber },
|
||||||
|
|
@ -145,11 +144,8 @@ class MigrationListScreenModel(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// in case it was removed
|
// in case it was removed
|
||||||
when (val migration = config.migration) {
|
if (manga.manga.id !in mangaIds) {
|
||||||
is MigrationType.MangaList -> if (manga.manga.id !in migration.mangaIds) {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
if (manga.searchResult.value == SearchResult.Searching && manga.migrationScope.isActive) {
|
||||||
|
|
@ -165,107 +161,27 @@ class MigrationListScreenModel(
|
||||||
} else {
|
} else {
|
||||||
sources.filter { it.id != mangaSource.id }
|
sources.filter { it.id != mangaSource.id }
|
||||||
}
|
}
|
||||||
when (val migration = config.migration) {
|
if (prioritizeByChapters) {
|
||||||
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) {
|
|
||||||
val sourceSemaphore = Semaphore(3)
|
val sourceSemaphore = Semaphore(3)
|
||||||
val processedSources = AtomicInteger()
|
val processedSources = AtomicInteger()
|
||||||
|
|
||||||
validSources.map { source ->
|
validSources.map { source ->
|
||||||
async async2@{
|
async async2@{
|
||||||
sourceSemaphore.withPermit {
|
sourceSemaphore.withPermit {
|
||||||
try {
|
val result = searchSource(manga.manga, source, deepSearchMode)
|
||||||
val searchResult = if (useSmartSearch) {
|
if (result != null) {
|
||||||
smartSearchEngine.smartSearch(source, mangaObj.ogTitle)
|
manga.progress.value =
|
||||||
} else {
|
validSources.size to processedSources.incrementAndGet()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.mapNotNull { it.await() }.maxByOrNull { it.second }?.first
|
}.mapNotNull { it.await() }.maxByOrNull { it.second }?.first
|
||||||
} else {
|
} else {
|
||||||
validSources.forEachIndexed { index, source ->
|
validSources.forEachIndexed { index, source ->
|
||||||
val searchResult = try {
|
val result = searchSource(manga.manga, source, deepSearchMode)
|
||||||
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
|
|
||||||
}
|
|
||||||
manga.progress.value = validSources.size to (index + 1)
|
manga.progress.value = validSources.size to (index + 1)
|
||||||
if (searchResult != null) return@async searchResult
|
if (result != null) return@async result.first
|
||||||
}
|
}
|
||||||
|
|
||||||
null
|
null
|
||||||
|
|
@ -294,37 +210,82 @@ class MigrationListScreenModel(
|
||||||
manga.searchResult.value = if (result == null) {
|
manga.searchResult.value = if (result == null) {
|
||||||
SearchResult.NotFound
|
SearchResult.NotFound
|
||||||
} else {
|
} else {
|
||||||
SearchResult.Result(result.id)
|
SearchResult.Success(result.id)
|
||||||
}
|
}
|
||||||
if (result == null && hideNotFound) {
|
if (result == null && hideUnmatched) {
|
||||||
removeManga(manga)
|
removeManga(manga)
|
||||||
}
|
}
|
||||||
if (result != null &&
|
if (result != null &&
|
||||||
showOnlyUpdates &&
|
hideWithoutUpdates &&
|
||||||
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
(getChapterInfo(result.id).latestChapter ?: 0.0) <= (manga.chapterInfo.latestChapter ?: 0.0)
|
||||||
) {
|
) {
|
||||||
removeManga(manga)
|
removeManga(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceFinished()
|
updateMigrationProgress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sourceFinished() {
|
private suspend fun searchSource(
|
||||||
finishedCount.value = migratingItems.value.orEmpty().count {
|
manga: Manga,
|
||||||
it.searchResult.value != SearchResult.Searching
|
source: CatalogueSource,
|
||||||
}
|
deepSearchMode: Boolean,
|
||||||
if (allMangasDone()) {
|
): Pair<Manga, Int>? {
|
||||||
migrationDone.value = true
|
return try {
|
||||||
}
|
val searchResult = if (deepSearchMode) {
|
||||||
if (migratingItems.value?.isEmpty() == true) {
|
smartSearchEngine.deepSearch(source, manga.ogTitle)
|
||||||
navigateOut()
|
} 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 } &&
|
private suspend fun updateMigrationProgress() {
|
||||||
migratingItems.value.orEmpty().any { it.searchResult.value is SearchResult.Result }
|
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 }
|
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")
|
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 {
|
} else {
|
||||||
migratingManga.searchResult.value = SearchResult.NotFound
|
migratingManga.searchResult.value = SearchResult.NotFound
|
||||||
withUIContext {
|
withUIContext {
|
||||||
|
|
@ -366,7 +327,7 @@ class MigrationListScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
sourceFinished()
|
updateMigrationProgress()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -385,18 +346,18 @@ class MigrationListScreenModel(
|
||||||
migratingProgress.value = 0f
|
migratingProgress.value = 0f
|
||||||
val items = migratingItems.value.orEmpty()
|
val items = migratingItems.value.orEmpty()
|
||||||
try {
|
try {
|
||||||
items.forEachIndexed { index, current ->
|
items.forEachIndexed { index, manga ->
|
||||||
try {
|
try {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
val target = current.searchResult.value.let {
|
val target = manga.searchResult.value.let {
|
||||||
if (it is SearchResult.Result) {
|
if (it is SearchResult.Success) {
|
||||||
getManga.await(it.id)
|
getManga.await(it.id)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
migrateManga(current.manga, target, replace)
|
migrateManga(manga.manga, target, replace)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
|
|
@ -405,7 +366,7 @@ class MigrationListScreenModel(
|
||||||
migratingProgress.value = index.toFloat() / items.size
|
migratingProgress.value = index.toFloat() / items.size
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateOut()
|
navigateBack()
|
||||||
} finally {
|
} finally {
|
||||||
migratingProgress.value = Float.MAX_VALUE
|
migratingProgress.value = Float.MAX_VALUE
|
||||||
migrateJob = null
|
migrateJob = null
|
||||||
|
|
@ -418,20 +379,20 @@ class MigrationListScreenModel(
|
||||||
migrateJob = null
|
migrateJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateOut() {
|
private suspend fun navigateBack() {
|
||||||
navigateOut.emit(Unit)
|
navigateOut.emit(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun migrateManga(mangaId: Long, replace: Boolean) {
|
fun migrateNow(mangaId: Long, replace: Boolean) {
|
||||||
manualMigrations.value++
|
manualMigrations.value++
|
||||||
screenModelScope.launchIO {
|
screenModelScope.launchIO {
|
||||||
val current = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
val manga = migratingItems.value.orEmpty().find { it.manga.id == mangaId }
|
||||||
?: return@launchIO
|
?: 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
|
?: return@launchIO
|
||||||
|
|
||||||
migrateManga(current.manga, target, replace)
|
migrateManga(manga.manga, target, replace)
|
||||||
removeManga(mangaId)
|
removeManga(mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -445,7 +406,7 @@ class MigrationListScreenModel(
|
||||||
item.searchingJob?.cancel()
|
item.searchingJob?.cancel()
|
||||||
item.searchingJob = null
|
item.searchingJob = null
|
||||||
item.searchResult.value = SearchResult.NotFound
|
item.searchResult.value = SearchResult.NotFound
|
||||||
sourceFinished()
|
updateMigrationProgress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -456,23 +417,18 @@ class MigrationListScreenModel(
|
||||||
?: return@launchIO
|
?: return@launchIO
|
||||||
removeManga(item)
|
removeManga(item)
|
||||||
item.migrationScope.cancel()
|
item.migrationScope.cancel()
|
||||||
sourceFinished()
|
updateMigrationProgress()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeManga(item: MigratingManga) {
|
private fun removeManga(item: MigratingManga) {
|
||||||
when (val migration = config.migration) {
|
val ids = mangaIds.toMutableList()
|
||||||
is MigrationType.MangaList -> {
|
val index = ids.indexOf(item.manga.id)
|
||||||
val ids = migration.mangaIds.toMutableList()
|
if (index > -1) {
|
||||||
val index = ids.indexOf(item.manga.id)
|
ids.removeAt(index)
|
||||||
if (index > -1) {
|
mangaIds = ids
|
||||||
ids.removeAt(index)
|
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
||||||
config.migration = MigrationType.MangaList(ids)
|
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
||||||
val index2 = migratingItems.value.orEmpty().indexOf(item)
|
|
||||||
if (index2 > -1) migratingItems.value = (migratingItems.value.orEmpty() - item).toImmutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MigrationType.MangaSingle -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,10 +439,8 @@ class MigrationListScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openMigrateDialog(
|
fun showMigrateDialog(copy: Boolean) {
|
||||||
copy: Boolean,
|
dialog.value = Dialog.Migrate(
|
||||||
) {
|
|
||||||
dialog.value = Dialog.MigrateMangaDialog(
|
|
||||||
copy,
|
copy,
|
||||||
migratingItems.value.orEmpty().size,
|
migratingItems.value.orEmpty().size,
|
||||||
mangasSkipped(),
|
mangasSkipped(),
|
||||||
|
|
@ -494,24 +448,24 @@ class MigrationListScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
fun openMigrationOptionsDialog() {
|
fun openOptionsDialog() {
|
||||||
dialog.value = Dialog.MigrationOptionsDialog
|
dialog.value = Dialog.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateOptions() {
|
fun updateOptions() {
|
||||||
hideNotFound = preferences.migrationHideUnmatched().get()
|
hideUnmatched = preferences.migrationHideUnmatched().get()
|
||||||
showOnlyUpdates = preferences.migrationHideWithoutUpdates().get()
|
hideWithoutUpdates = preferences.migrationHideWithoutUpdates().get()
|
||||||
useSourceWithMost = preferences.migrationPrioritizeByChapters().get()
|
prioritizeByChapters = preferences.migrationPrioritizeByChapters().get()
|
||||||
useSmartSearch = preferences.migrationDeepSearchMode().get()
|
deepSearchMode = preferences.migrationDeepSearchMode().get()
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
sealed class Dialog {
|
sealed class Dialog {
|
||||||
data class MigrateMangaDialog(val copy: Boolean, val mangaSet: Int, val mangaSkipped: Int) : Dialog()
|
data class Migrate(val copy: Boolean, val totalCount: Int, val skippedCount: Int) : Dialog()
|
||||||
data object MigrationExitDialog : Dialog()
|
data object Exit : Dialog()
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
data object MigrationOptionsDialog : Dialog()
|
data object Options : Dialog()
|
||||||
// KMK <--
|
// 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(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(SYMR.strings.stop_migrating))
|
||||||
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = exitMigration) {
|
TextButton(onClick = exitMigration) {
|
||||||
Text(text = stringResource(SYMR.strings.action_stop))
|
Text(text = stringResource(SYMR.strings.action_stop))
|
||||||
|
|
@ -25,8 +28,5 @@ fun MigrationExitDialog(
|
||||||
Text(text = stringResource(MR.strings.action_cancel))
|
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(
|
fun MigrationMangaDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
copy: Boolean,
|
copy: Boolean,
|
||||||
mangaSet: Int,
|
totalCount: Int,
|
||||||
mangaSkipped: Int,
|
skippedCount: Int,
|
||||||
copyManga: () -> Unit,
|
copyManga: () -> Unit,
|
||||||
migrateManga: () -> Unit,
|
onMigrate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
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 = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (copy) {
|
if (copy) {
|
||||||
copyManga()
|
copyManga()
|
||||||
} else {
|
} else {
|
||||||
migrateManga()
|
onMigrate()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
@ -38,15 +48,5 @@ fun MigrationMangaDialog(
|
||||||
Text(text = stringResource(MR.strings.action_cancel))
|
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(
|
class MigratingManga(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val chapterInfo: ChapterInfo,
|
val chapterInfo: ChapterInfo,
|
||||||
val sourcesString: String,
|
val source: String,
|
||||||
parentContext: CoroutineContext,
|
parentContext: CoroutineContext,
|
||||||
) {
|
) {
|
||||||
val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default)
|
val migrationScope = CoroutineScope(parentContext + SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
@ -33,7 +33,7 @@ class MigratingManga(
|
||||||
sealed class SearchResult {
|
sealed class SearchResult {
|
||||||
data object Searching : SearchResult()
|
data object Searching : SearchResult()
|
||||||
data object NotFound : SearchResult()
|
data object NotFound : SearchResult()
|
||||||
data class Result(val id: Long) : SearchResult()
|
data class Success(val id: Long) : SearchResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChapterInfo(
|
data class ChapterInfo(
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ abstract class BaseSmartSearchEngine<T>(
|
||||||
|
|
||||||
protected abstract fun getTitle(result: T): String
|
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 cleanedTitle = cleanSmartSearchTitle(title)
|
||||||
|
|
||||||
val queries = getSmartSearchQueries(cleanedTitle)
|
val queries = getDeepSearchQueries(cleanedTitle)
|
||||||
|
|
||||||
val eligibleManga = supervisorScope {
|
val eligibleManga = supervisorScope {
|
||||||
queries.map { query ->
|
queries.map { query ->
|
||||||
|
|
@ -42,10 +42,10 @@ abstract class BaseSmartSearchEngine<T>(
|
||||||
}.flatMap { it.await() }
|
}.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 eligibleManga = supervisorScope {
|
||||||
val searchQuery = if (extraSearchParams != null) {
|
val searchQuery = if (extraSearchParams != null) {
|
||||||
"$title ${extraSearchParams.trim()}"
|
"$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 {
|
private fun cleanSmartSearchTitle(title: String): String {
|
||||||
|
|
@ -142,7 +142,7 @@ abstract class BaseSmartSearchEngine<T>(
|
||||||
return result.toString()
|
return result.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSmartSearchQueries(cleanedTitle: String): List<String> {
|
private fun getDeepSearchQueries(cleanedTitle: String): List<String> {
|
||||||
val splitCleanedTitle = cleanedTitle.split(" ")
|
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||||
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||||
|
|
||||||
|
|
@ -155,7 +155,6 @@ abstract class BaseSmartSearchEngine<T>(
|
||||||
// Search largest word
|
// Search largest word
|
||||||
// Search first two words
|
// Search first two words
|
||||||
// Search first word
|
// Search first word
|
||||||
|
|
||||||
val searchQueries = listOf(
|
val searchQueries = listOf(
|
||||||
listOf(cleanedTitle),
|
listOf(cleanedTitle),
|
||||||
splitSortedByLargest.take(2),
|
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
|
override fun getTitle(result: SManga) = result.originalTitle
|
||||||
|
|
||||||
suspend fun smartSearch(source: CatalogueSource, title: String): Manga? =
|
suspend fun regularSearch(source: CatalogueSource, title: String): Manga? =
|
||||||
smartSearch(makeSearchAction(source), title).let {
|
regularSearch(makeSearchAction(source), title).let {
|
||||||
it?.toDomainManga(source.id)
|
it?.toDomainManga(source.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun normalSearch(source: CatalogueSource, title: String): Manga? =
|
suspend fun deepSearch(source: CatalogueSource, title: String): Manga? =
|
||||||
normalSearch(makeSearchAction(source), title).let {
|
deepSearch(makeSearchAction(source), title).let {
|
||||||
it?.toDomainManga(source.id)
|
it?.toDomainManga(source.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue