Support mass migration for selected library items (#2336)

This commit is contained in:
AntsyLich
2025-08-03 01:03:45 +06:00
committed by GitHub
parent e62cd0e816
commit 982ebcf777
7 changed files with 128 additions and 44 deletions

View File

@@ -24,6 +24,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Add advanced option to always update manga title from source ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1182](https://github.com/mihonapp/mihon/pull/1182))
- Full predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2085](https://github.com/mihonapp/mihon/pull/2085))
- Add Catppuccin theme (mocha for dark and latte for light, mauve accent) ([@claymorwan](https://github.com/claymorwan/)) ([#2117](https://github.com/mihonapp/mihon/pull/2117))
- Manga mass migration ([@AntsyLich](https://github.com/AntsyLich), [@jobobby04](https://github.com/jobobby04)) ([#2110](https://github.com/mihonapp/mihon/pull/2110), [#2336](https://github.com/mihonapp/mihon/pull/2336))
### Improved
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946))

View File

@@ -1,9 +1,11 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
@@ -15,7 +17,41 @@ fun DownloadDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
offset: DpOffset? = null,
modifier: Modifier = Modifier,
) {
if (offset != null) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier,
offset = offset,
content = {
DownloadDropdownMenuItems(
onDismissRequest = onDismissRequest,
onDownloadClicked = onDownloadClicked,
)
},
)
} else {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier,
content = {
DownloadDropdownMenuItems(
onDismissRequest = onDismissRequest,
onDownloadClicked = onDownloadClicked,
)
},
)
}
}
@Composable
private fun ColumnScope.DownloadDropdownMenuItems(
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
) {
val options = persistentListOf(
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
@@ -25,11 +61,6 @@ fun DownloadDropdownMenu(
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
)
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
options.map { (downloadAction, string) ->
DropdownMenuItem(
text = { Text(text = string) },
@@ -40,4 +71,3 @@ fun DownloadDropdownMenu(
)
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -28,7 +29,10 @@ import androidx.compose.material.icons.outlined.BookmarkRemove
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -48,8 +52,10 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R
import kotlinx.coroutines.Job
@@ -185,7 +191,7 @@ private fun RowScope.Button(
targetValue = if (toConfirm) 2f else 1f,
label = "weight",
)
Column(
Box(
modifier = Modifier
.size(48.dp)
.weight(animatedWeight)
@@ -195,6 +201,9 @@ private fun RowScope.Button(
onLongClick = onLongClick,
onClick = onClick,
),
contentAlignment = Alignment.Center,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -214,6 +223,7 @@ private fun RowScope.Button(
style = MaterialTheme.typography.labelSmall,
)
}
}
content?.invoke()
}
}
@@ -226,6 +236,7 @@ fun LibraryBottomActionMenu(
onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit,
onMigrateClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
@@ -240,17 +251,18 @@ fun LibraryBottomActionMenu(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false
}
}
val itemOverflow = onDownloadClicked != null
Row(
modifier = Modifier
.windowInsetsPadding(
@@ -289,22 +301,57 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(3) },
onClick = { downloadExpanded = !downloadExpanded },
) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDismissRequest = { downloadExpanded = false },
onDownloadClicked = onDownloadClicked,
offset = BottomBarMenuDpOffset,
)
}
}
if (!itemOverflow) {
Button(
title = stringResource(MR.strings.migrate),
icon = Icons.Outlined.SwapCalls,
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
onClick = onMigrateClicked,
)
Button(
title = stringResource(MR.strings.action_delete),
icon = Icons.Outlined.Delete,
toConfirm = confirm[4],
onLongClick = { onLongClickItem(4) },
toConfirm = confirm[5],
onLongClick = { onLongClickItem(5) },
onClick = onDeleteClicked,
)
} else {
var overflowMenuOpen by remember { mutableStateOf(false) }
Button(
title = stringResource(MR.strings.label_more),
icon = Icons.Outlined.MoreVert,
toConfirm = false,
onLongClick = {},
onClick = { overflowMenuOpen = true },
) {
DropdownMenu(
expanded = overflowMenuOpen,
onDismissRequest = { overflowMenuOpen = false },
offset = BottomBarMenuDpOffset,
) {
DropdownMenuItem(
text = { Text(stringResource(MR.strings.migrate)) },
onClick = onMigrateClicked,
)
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_delete)) },
onClick = onDeleteClicked,
)
}
}
}
}
}
}
}
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)

View File

@@ -48,6 +48,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import mihon.feature.migration.config.MigrationConfigScreen
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.category.model.Category
@@ -149,6 +150,11 @@ data object LibraryTab : Tab {
onDownloadClicked = screenModel::performDownloadAction
.takeIf { state.selectedManga.fastAll { !it.isLocal() } },
onDeleteClicked = screenModel::openDeleteMangaDialog,
onMigrateClicked = {
val selection = state.selection
screenModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },

View File

@@ -70,7 +70,7 @@ import tachiyomi.presentation.core.util.shouldExpandFAB
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationConfigScreen(private val mangaIds: List<Long>) : Screen() {
class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
constructor(mangaId: Long) : this(listOf(mangaId))

View File

@@ -19,7 +19,7 @@ import mihon.feature.migration.list.components.MigrationMangaDialog
import mihon.feature.migration.list.components.MigrationProgressDialog
import tachiyomi.i18n.MR
class MigrationListScreen(private val mangaIds: List<Long>, private val extraSearchQuery: String?) : Screen() {
class MigrationListScreen(private val mangaIds: Collection<Long>, private val extraSearchQuery: String?) : Screen() {
private var matchOverride: Pair<Long, Long>? = null

View File

@@ -43,7 +43,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationListScreenModel(
mangaIds: List<Long>,
mangaIds: Collection<Long>,
extraSearchQuery: String?,
private val preferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),