package eu.kanade.presentation.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BookmarkAdd import androidx.compose.material.icons.filled.BookmarkRemove import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.RemoveDone import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Label import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @Composable fun MangaBottomActionMenu( visible: Boolean, modifier: Modifier = Modifier, onBookmarkClicked: (() -> Unit)? = null, onRemoveBookmarkClicked: (() -> Unit)? = null, onMarkAsReadClicked: (() -> Unit)? = null, onMarkAsUnreadClicked: (() -> Unit)? = null, onMarkPreviousAsReadClicked: (() -> Unit)? = null, onDownloadClicked: (() -> Unit)? = null, onDeleteClicked: (() -> Unit)? = null, ) { AnimatedVisibility( visible = visible, enter = expandVertically(expandFrom = Alignment.Bottom), exit = shrinkVertically(shrinkTowards = Alignment.Bottom), ) { val scope = rememberCoroutineScope() Surface( modifier = modifier, shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), tonalElevation = 3.dp, ) { val haptic = LocalHapticFeedback.current val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) } var resetJob: Job? = remember { null } val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> haptic.performHapticFeedback(HapticFeedbackType.LongPress) (0 until 7).forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() resetJob = scope.launch { delay(1000) if (isActive) confirm[toConfirmIndex] = false } } Row( modifier = Modifier .navigationBarsPadding() .padding(horizontal = 8.dp, vertical = 12.dp), ) { if (onBookmarkClicked != null) { Button( title = stringResource(R.string.action_bookmark), icon = Icons.Default.BookmarkAdd, toConfirm = confirm[0], onLongClick = { onLongClickItem(0) }, onClick = onBookmarkClicked, ) } if (onRemoveBookmarkClicked != null) { Button( title = stringResource(R.string.action_remove_bookmark), icon = Icons.Default.BookmarkRemove, toConfirm = confirm[1], onLongClick = { onLongClickItem(1) }, onClick = onRemoveBookmarkClicked, ) } if (onMarkAsReadClicked != null) { Button( title = stringResource(R.string.action_mark_as_read), icon = Icons.Default.DoneAll, toConfirm = confirm[2], onLongClick = { onLongClickItem(2) }, onClick = onMarkAsReadClicked, ) } if (onMarkAsUnreadClicked != null) { Button( title = stringResource(R.string.action_mark_as_unread), icon = Icons.Default.RemoveDone, toConfirm = confirm[3], onLongClick = { onLongClickItem(3) }, onClick = onMarkAsUnreadClicked, ) } if (onMarkPreviousAsReadClicked != null) { Button( title = stringResource(R.string.action_mark_previous_as_read), icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp), toConfirm = confirm[4], onLongClick = { onLongClickItem(4) }, onClick = onMarkPreviousAsReadClicked, ) } if (onDownloadClicked != null) { Button( title = stringResource(R.string.action_download), icon = Icons.Outlined.Download, toConfirm = confirm[5], onLongClick = { onLongClickItem(5) }, onClick = onDownloadClicked, ) } if (onDeleteClicked != null) { Button( title = stringResource(R.string.action_delete), icon = Icons.Outlined.Delete, toConfirm = confirm[6], onLongClick = { onLongClickItem(6) }, onClick = onDeleteClicked, ) } } } } } @Composable private fun RowScope.Button( title: String, icon: ImageVector, toConfirm: Boolean, onLongClick: () -> Unit, onClick: () -> Unit, ) { val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f) Column( modifier = Modifier .size(48.dp) .weight(animatedWeight) .combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), onLongClick = onLongClick, onClick = onClick, ), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { 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, ) } } } @Composable fun LibraryBottomActionMenu( visible: Boolean, modifier: Modifier = Modifier, onChangeCategoryClicked: (() -> Unit)?, onMarkAsReadClicked: (() -> Unit)?, onMarkAsUnreadClicked: (() -> Unit)?, onDownloadClicked: (() -> Unit)?, onDeleteClicked: (() -> Unit)?, ) { AnimatedVisibility( visible = visible, enter = expandVertically(expandFrom = Alignment.Bottom), exit = shrinkVertically(shrinkTowards = Alignment.Bottom), ) { val scope = rememberCoroutineScope() Surface( modifier = modifier, shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize), tonalElevation = 3.dp, ) { val haptic = LocalHapticFeedback.current val confirm = remember { mutableStateListOf(false, false, false, false, false) } var resetJob: Job? = remember { null } val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> haptic.performHapticFeedback(HapticFeedbackType.LongPress) (0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex } resetJob?.cancel() resetJob = scope.launch { delay(1000) if (isActive) confirm[toConfirmIndex] = false } } Row( modifier = Modifier .navigationBarsPadding() .padding(horizontal = 8.dp, vertical = 12.dp), ) { if (onChangeCategoryClicked != null) { Button( title = stringResource(R.string.action_move_category), icon = Icons.Outlined.Label, toConfirm = confirm[0], onLongClick = { onLongClickItem(0) }, onClick = onChangeCategoryClicked, ) } if (onMarkAsReadClicked != null) { Button( title = stringResource(R.string.action_mark_as_read), icon = Icons.Default.DoneAll, toConfirm = confirm[1], onLongClick = { onLongClickItem(1) }, onClick = onMarkAsReadClicked, ) } if (onMarkAsUnreadClicked != null) { Button( title = stringResource(R.string.action_mark_as_unread), icon = Icons.Default.RemoveDone, toConfirm = confirm[2], onLongClick = { onLongClickItem(2) }, onClick = onMarkAsUnreadClicked, ) } if (onDownloadClicked != null) { Button( title = stringResource(R.string.action_download), icon = Icons.Outlined.Download, toConfirm = confirm[3], onLongClick = { onLongClickItem(3) }, onClick = onDownloadClicked, ) } if (onDeleteClicked != null) { Button( title = stringResource(R.string.action_delete), icon = Icons.Outlined.Delete, toConfirm = confirm[4], onLongClick = { onLongClickItem(4) }, onClick = onDeleteClicked, ) } } } } }