mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Support mass migration for selected library items (#2336)
This commit is contained in:
		| @@ -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() | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) }, | ||||
|   | ||||
| @@ -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)) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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(), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user