mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Support mass migration for selected library items (#2336)
This commit is contained in:
@@ -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))
|
||||||
|
@@ -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,11 +61,6 @@ fun DownloadDropdownMenu(
|
|||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
options.map { (downloadAction, string) ->
|
options.map { (downloadAction, string) ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = string) },
|
text = { Text(text = string) },
|
||||||
@@ -40,4 +71,3 @@ fun DownloadDropdownMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@@ -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,6 +201,9 @@ private fun RowScope.Button(
|
|||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@@ -214,6 +223,7 @@ private fun RowScope.Button(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!itemOverflow) {
|
||||||
|
Button(
|
||||||
|
title = stringResource(MR.strings.migrate),
|
||||||
|
icon = Icons.Outlined.SwapCalls,
|
||||||
|
toConfirm = confirm[4],
|
||||||
|
onLongClick = { onLongClickItem(4) },
|
||||||
|
onClick = onMigrateClicked,
|
||||||
|
)
|
||||||
Button(
|
Button(
|
||||||
title = stringResource(MR.strings.action_delete),
|
title = stringResource(MR.strings.action_delete),
|
||||||
icon = Icons.Outlined.Delete,
|
icon = Icons.Outlined.Delete,
|
||||||
toConfirm = confirm[4],
|
toConfirm = confirm[5],
|
||||||
onLongClick = { onLongClickItem(4) },
|
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,
|
onClick = onDeleteClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val BottomBarMenuDpOffset = DpOffset(0.dp, 0.dp)
|
||||||
|
@@ -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) },
|
||||||
|
@@ -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))
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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(),
|
||||||
|
Reference in New Issue
Block a user