diff --git a/CHANGELOG.md b/CHANGELOG.md index c4d9790b8..3032129dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt index a116c3ee1..e3391ec5a 100644 --- a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt @@ -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,19 +61,13 @@ 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) }, - onClick = { - onDownloadClicked(downloadAction) - onDismissRequest() - }, - ) - } + options.map { (downloadAction, string) -> + DropdownMenuItem( + text = { Text(text = string) }, + onClick = { + onDownloadClicked(downloadAction) + onDismissRequest() + }, + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt index 93b8c1843..b59cbb016 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt @@ -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,24 +201,28 @@ private fun RowScope.Button( onLongClick = onLongClick, onClick = onClick, ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + contentAlignment = Alignment.Center, ) { - Icon( - imageVector = icon, - contentDescription = title, - ) - AnimatedVisibility( - visible = toConfirm, - enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), - exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Text( - text = title, - overflow = TextOverflow.Visible, - maxLines = 1, - style = MaterialTheme.typography.labelSmall, + Icon( + imageVector = icon, + contentDescription = title, ) + 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() } @@ -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, ) } } - Button( - title = stringResource(MR.strings.action_delete), - icon = Icons.Outlined.Delete, - toConfirm = confirm[4], - onLongClick = { onLongClickItem(4) }, - onClick = onDeleteClicked, - ) + 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[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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index cd2e5c01e..8598bf393 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -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) }, diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index 499a33397..580539c48 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -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) : Screen() { +class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { constructor(mangaId: Long) : this(listOf(mangaId)) diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt index 4774095d3..957b115cf 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreen.kt @@ -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, private val extraSearchQuery: String?) : Screen() { +class MigrationListScreen(private val mangaIds: Collection, private val extraSearchQuery: String?) : Screen() { private var matchOverride: Pair? = null diff --git a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt index 2bc216e8b..0276ea457 100644 --- a/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt +++ b/app/src/main/java/mihon/feature/migration/list/MigrationListScreenModel.kt @@ -43,7 +43,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MigrationListScreenModel( - mangaIds: List, + mangaIds: Collection, extraSearchQuery: String?, private val preferences: SourcePreferences = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),