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)) - 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)) - 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)) - 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 ### Improved
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946)) - 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 package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -15,7 +17,41 @@ fun DownloadDropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit, onDownloadClicked: (DownloadAction) -> Unit,
offset: DpOffset? = null,
modifier: Modifier = Modifier, 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( val options = persistentListOf(
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1), DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
@@ -25,19 +61,13 @@ fun DownloadDropdownMenu(
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread), DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
) )
DropdownMenu( options.map { (downloadAction, string) ->
expanded = expanded, DropdownMenuItem(
onDismissRequest = onDismissRequest, text = { Text(text = string) },
modifier = modifier, onClick = {
) { onDownloadClicked(downloadAction)
options.map { (downloadAction, string) -> onDismissRequest()
DropdownMenuItem( },
text = { Text(text = string) }, )
onClick = {
onDownloadClicked(downloadAction)
onDismissRequest()
},
)
}
} }
} }

View File

@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.Delete
import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Download 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.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface 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.platform.LocalHapticFeedback
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow 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.components.DownloadDropdownMenu import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -185,7 +191,7 @@ private fun RowScope.Button(
targetValue = if (toConfirm) 2f else 1f, targetValue = if (toConfirm) 2f else 1f,
label = "weight", label = "weight",
) )
Column( Box(
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.weight(animatedWeight) .weight(animatedWeight)
@@ -195,24 +201,28 @@ private fun RowScope.Button(
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,
), ),
verticalArrangement = Arrangement.Center, contentAlignment = Alignment.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Icon( Column(
imageVector = icon, verticalArrangement = Arrangement.Center,
contentDescription = title, horizontalAlignment = Alignment.CenterHorizontally,
)
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) { ) {
Text( Icon(
text = title, imageVector = icon,
overflow = TextOverflow.Visible, contentDescription = title,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
) )
AnimatedVisibility(
visible = toConfirm,
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
) {
Text(
text = title,
overflow = TextOverflow.Visible,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
)
}
} }
content?.invoke() content?.invoke()
} }
@@ -226,6 +236,7 @@ fun LibraryBottomActionMenu(
onMarkAsUnreadClicked: () -> Unit, onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?, onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit, onDeleteClicked: () -> Unit,
onMigrateClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
@@ -240,17 +251,18 @@ fun LibraryBottomActionMenu(
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = MaterialTheme.colorScheme.surfaceContainerHigh,
) { ) {
val haptic = LocalHapticFeedback.current 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 } var resetJob: Job? = remember { null }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex } (0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel() resetJob?.cancel()
resetJob = scope.launch { resetJob = scope.launch {
delay(1.seconds) delay(1.seconds)
if (isActive) confirm[toConfirmIndex] = false if (isActive) confirm[toConfirmIndex] = false
} }
} }
val itemOverflow = onDownloadClicked != null
Row( Row(
modifier = Modifier modifier = Modifier
.windowInsetsPadding( .windowInsetsPadding(
@@ -289,22 +301,57 @@ fun LibraryBottomActionMenu(
onLongClick = { onLongClickItem(3) }, onLongClick = { onLongClickItem(3) },
onClick = { downloadExpanded = !downloadExpanded }, onClick = { downloadExpanded = !downloadExpanded },
) { ) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu( DownloadDropdownMenu(
expanded = downloadExpanded, expanded = downloadExpanded,
onDismissRequest = onDismissRequest, onDismissRequest = { downloadExpanded = false },
onDownloadClicked = onDownloadClicked, onDownloadClicked = onDownloadClicked,
offset = BottomBarMenuDpOffset,
) )
} }
} }
Button( if (!itemOverflow) {
title = stringResource(MR.strings.action_delete), Button(
icon = Icons.Outlined.Delete, title = stringResource(MR.strings.migrate),
toConfirm = confirm[4], icon = Icons.Outlined.SwapCalls,
onLongClick = { onLongClickItem(4) }, toConfirm = confirm[4],
onClick = onDeleteClicked, onLongClick = { onLongClickItem(4) },
) onClick = onMigrateClicked,
)
Button(
title = stringResource(MR.strings.action_delete),
icon = Icons.Outlined.Delete,
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.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.feature.migration.config.MigrationConfigScreen
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@@ -149,6 +150,11 @@ data object LibraryTab : Tab {
onDownloadClicked = screenModel::performDownloadAction onDownloadClicked = screenModel::performDownloadAction
.takeIf { state.selectedManga.fastAll { !it.isLocal() } }, .takeIf { state.selectedManga.fastAll { !it.isLocal() } },
onDeleteClicked = screenModel::openDeleteMangaDialog, onDeleteClicked = screenModel::openDeleteMangaDialog,
onMigrateClicked = {
val selection = state.selection
screenModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
) )
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, 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.Injekt
import uy.kohesive.injekt.api.get 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)) 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 mihon.feature.migration.list.components.MigrationProgressDialog
import tachiyomi.i18n.MR 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 private var matchOverride: Pair<Long, Long>? = null

View File

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