MangaBottomActionMenu.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package eu.kanade.presentation.manga.components
  2. import androidx.compose.animation.AnimatedVisibility
  3. import androidx.compose.animation.core.animateFloatAsState
  4. import androidx.compose.animation.core.tween
  5. import androidx.compose.animation.expandVertically
  6. import androidx.compose.animation.fadeIn
  7. import androidx.compose.animation.fadeOut
  8. import androidx.compose.animation.shrinkVertically
  9. import androidx.compose.foundation.combinedClickable
  10. import androidx.compose.foundation.interaction.MutableInteractionSource
  11. import androidx.compose.foundation.layout.Arrangement
  12. import androidx.compose.foundation.layout.Column
  13. import androidx.compose.foundation.layout.Row
  14. import androidx.compose.foundation.layout.RowScope
  15. import androidx.compose.foundation.layout.WindowInsets
  16. import androidx.compose.foundation.layout.WindowInsetsSides
  17. import androidx.compose.foundation.layout.asPaddingValues
  18. import androidx.compose.foundation.layout.navigationBars
  19. import androidx.compose.foundation.layout.only
  20. import androidx.compose.foundation.layout.padding
  21. import androidx.compose.foundation.layout.size
  22. import androidx.compose.foundation.layout.windowInsetsPadding
  23. import androidx.compose.foundation.shape.ZeroCornerSize
  24. import androidx.compose.material.icons.Icons
  25. import androidx.compose.material.icons.automirrored.outlined.Label
  26. import androidx.compose.material.icons.outlined.BookmarkAdd
  27. import androidx.compose.material.icons.outlined.BookmarkRemove
  28. import androidx.compose.material.icons.outlined.Delete
  29. import androidx.compose.material.icons.outlined.DoneAll
  30. import androidx.compose.material.icons.outlined.Download
  31. import androidx.compose.material.icons.outlined.Label
  32. import androidx.compose.material.icons.outlined.RemoveDone
  33. import androidx.compose.material.ripple.rememberRipple
  34. import androidx.compose.material3.Icon
  35. import androidx.compose.material3.MaterialTheme
  36. import androidx.compose.material3.Surface
  37. import androidx.compose.material3.Text
  38. import androidx.compose.runtime.Composable
  39. import androidx.compose.runtime.getValue
  40. import androidx.compose.runtime.mutableStateListOf
  41. import androidx.compose.runtime.mutableStateOf
  42. import androidx.compose.runtime.remember
  43. import androidx.compose.runtime.rememberCoroutineScope
  44. import androidx.compose.runtime.setValue
  45. import androidx.compose.ui.Alignment
  46. import androidx.compose.ui.Modifier
  47. import androidx.compose.ui.graphics.vector.ImageVector
  48. import androidx.compose.ui.hapticfeedback.HapticFeedbackType
  49. import androidx.compose.ui.platform.LocalHapticFeedback
  50. import androidx.compose.ui.res.vectorResource
  51. import androidx.compose.ui.text.style.TextOverflow
  52. import androidx.compose.ui.unit.dp
  53. import eu.kanade.presentation.components.DownloadDropdownMenu
  54. import eu.kanade.presentation.manga.DownloadAction
  55. import eu.kanade.tachiyomi.R
  56. import kotlinx.coroutines.Job
  57. import kotlinx.coroutines.delay
  58. import kotlinx.coroutines.isActive
  59. import kotlinx.coroutines.launch
  60. import tachiyomi.i18n.MR
  61. import tachiyomi.presentation.core.i18n.localize
  62. import kotlin.time.Duration.Companion.seconds
  63. @Composable
  64. fun MangaBottomActionMenu(
  65. visible: Boolean,
  66. modifier: Modifier = Modifier,
  67. onBookmarkClicked: (() -> Unit)? = null,
  68. onRemoveBookmarkClicked: (() -> Unit)? = null,
  69. onMarkAsReadClicked: (() -> Unit)? = null,
  70. onMarkAsUnreadClicked: (() -> Unit)? = null,
  71. onMarkPreviousAsReadClicked: (() -> Unit)? = null,
  72. onDownloadClicked: (() -> Unit)? = null,
  73. onDeleteClicked: (() -> Unit)? = null,
  74. ) {
  75. AnimatedVisibility(
  76. visible = visible,
  77. enter = expandVertically(expandFrom = Alignment.Bottom),
  78. exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
  79. ) {
  80. val scope = rememberCoroutineScope()
  81. Surface(
  82. modifier = modifier,
  83. shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
  84. tonalElevation = 3.dp,
  85. ) {
  86. val haptic = LocalHapticFeedback.current
  87. val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
  88. var resetJob: Job? = remember { null }
  89. val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
  90. haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  91. (0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
  92. resetJob?.cancel()
  93. resetJob = scope.launch {
  94. delay(1.seconds)
  95. if (isActive) confirm[toConfirmIndex] = false
  96. }
  97. }
  98. Row(
  99. modifier = Modifier
  100. .padding(
  101. WindowInsets.navigationBars
  102. .only(WindowInsetsSides.Bottom)
  103. .asPaddingValues(),
  104. )
  105. .padding(horizontal = 8.dp, vertical = 12.dp),
  106. ) {
  107. if (onBookmarkClicked != null) {
  108. Button(
  109. title = localize(MR.strings.action_bookmark),
  110. icon = Icons.Outlined.BookmarkAdd,
  111. toConfirm = confirm[0],
  112. onLongClick = { onLongClickItem(0) },
  113. onClick = onBookmarkClicked,
  114. )
  115. }
  116. if (onRemoveBookmarkClicked != null) {
  117. Button(
  118. title = localize(MR.strings.action_remove_bookmark),
  119. icon = Icons.Outlined.BookmarkRemove,
  120. toConfirm = confirm[1],
  121. onLongClick = { onLongClickItem(1) },
  122. onClick = onRemoveBookmarkClicked,
  123. )
  124. }
  125. if (onMarkAsReadClicked != null) {
  126. Button(
  127. title = localize(MR.strings.action_mark_as_read),
  128. icon = Icons.Outlined.DoneAll,
  129. toConfirm = confirm[2],
  130. onLongClick = { onLongClickItem(2) },
  131. onClick = onMarkAsReadClicked,
  132. )
  133. }
  134. if (onMarkAsUnreadClicked != null) {
  135. Button(
  136. title = localize(MR.strings.action_mark_as_unread),
  137. icon = Icons.Outlined.RemoveDone,
  138. toConfirm = confirm[3],
  139. onLongClick = { onLongClickItem(3) },
  140. onClick = onMarkAsUnreadClicked,
  141. )
  142. }
  143. if (onMarkPreviousAsReadClicked != null) {
  144. Button(
  145. title = localize(MR.strings.action_mark_previous_as_read),
  146. icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
  147. toConfirm = confirm[4],
  148. onLongClick = { onLongClickItem(4) },
  149. onClick = onMarkPreviousAsReadClicked,
  150. )
  151. }
  152. if (onDownloadClicked != null) {
  153. Button(
  154. title = localize(MR.strings.action_download),
  155. icon = Icons.Outlined.Download,
  156. toConfirm = confirm[5],
  157. onLongClick = { onLongClickItem(5) },
  158. onClick = onDownloadClicked,
  159. )
  160. }
  161. if (onDeleteClicked != null) {
  162. Button(
  163. title = localize(MR.strings.action_delete),
  164. icon = Icons.Outlined.Delete,
  165. toConfirm = confirm[6],
  166. onLongClick = { onLongClickItem(6) },
  167. onClick = onDeleteClicked,
  168. )
  169. }
  170. }
  171. }
  172. }
  173. }
  174. @Composable
  175. private fun RowScope.Button(
  176. title: String,
  177. icon: ImageVector,
  178. toConfirm: Boolean,
  179. onLongClick: () -> Unit,
  180. onClick: () -> Unit,
  181. content: (@Composable () -> Unit)? = null,
  182. ) {
  183. val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
  184. Column(
  185. modifier = Modifier
  186. .size(48.dp)
  187. .weight(animatedWeight)
  188. .combinedClickable(
  189. interactionSource = remember { MutableInteractionSource() },
  190. indication = rememberRipple(bounded = false),
  191. onLongClick = onLongClick,
  192. onClick = onClick,
  193. ),
  194. verticalArrangement = Arrangement.Center,
  195. horizontalAlignment = Alignment.CenterHorizontally,
  196. ) {
  197. Icon(
  198. imageVector = icon,
  199. contentDescription = title,
  200. )
  201. AnimatedVisibility(
  202. visible = toConfirm,
  203. enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
  204. exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
  205. ) {
  206. Text(
  207. text = title,
  208. overflow = TextOverflow.Visible,
  209. maxLines = 1,
  210. style = MaterialTheme.typography.labelSmall,
  211. )
  212. }
  213. content?.invoke()
  214. }
  215. }
  216. @Composable
  217. fun LibraryBottomActionMenu(
  218. visible: Boolean,
  219. modifier: Modifier = Modifier,
  220. onChangeCategoryClicked: () -> Unit,
  221. onMarkAsReadClicked: () -> Unit,
  222. onMarkAsUnreadClicked: () -> Unit,
  223. onDownloadClicked: ((DownloadAction) -> Unit)?,
  224. onDeleteClicked: () -> Unit,
  225. ) {
  226. AnimatedVisibility(
  227. visible = visible,
  228. enter = expandVertically(animationSpec = tween(delayMillis = 300)),
  229. exit = shrinkVertically(animationSpec = tween()),
  230. ) {
  231. val scope = rememberCoroutineScope()
  232. Surface(
  233. modifier = modifier,
  234. shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
  235. tonalElevation = 3.dp,
  236. ) {
  237. val haptic = LocalHapticFeedback.current
  238. val confirm = remember { mutableStateListOf(false, false, false, false, false) }
  239. var resetJob: Job? = remember { null }
  240. val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
  241. haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  242. (0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
  243. resetJob?.cancel()
  244. resetJob = scope.launch {
  245. delay(1.seconds)
  246. if (isActive) confirm[toConfirmIndex] = false
  247. }
  248. }
  249. Row(
  250. modifier = Modifier
  251. .windowInsetsPadding(
  252. WindowInsets.navigationBars
  253. .only(WindowInsetsSides.Bottom),
  254. )
  255. .padding(horizontal = 8.dp, vertical = 12.dp),
  256. ) {
  257. Button(
  258. title = localize(MR.strings.action_move_category),
  259. icon = Icons.AutoMirrored.Outlined.Label,
  260. toConfirm = confirm[0],
  261. onLongClick = { onLongClickItem(0) },
  262. onClick = onChangeCategoryClicked,
  263. )
  264. Button(
  265. title = localize(MR.strings.action_mark_as_read),
  266. icon = Icons.Outlined.DoneAll,
  267. toConfirm = confirm[1],
  268. onLongClick = { onLongClickItem(1) },
  269. onClick = onMarkAsReadClicked,
  270. )
  271. Button(
  272. title = localize(MR.strings.action_mark_as_unread),
  273. icon = Icons.Outlined.RemoveDone,
  274. toConfirm = confirm[2],
  275. onLongClick = { onLongClickItem(2) },
  276. onClick = onMarkAsUnreadClicked,
  277. )
  278. if (onDownloadClicked != null) {
  279. var downloadExpanded by remember { mutableStateOf(false) }
  280. Button(
  281. title = localize(MR.strings.action_download),
  282. icon = Icons.Outlined.Download,
  283. toConfirm = confirm[3],
  284. onLongClick = { onLongClickItem(3) },
  285. onClick = { downloadExpanded = !downloadExpanded },
  286. ) {
  287. val onDismissRequest = { downloadExpanded = false }
  288. DownloadDropdownMenu(
  289. expanded = downloadExpanded,
  290. onDismissRequest = onDismissRequest,
  291. onDownloadClicked = onDownloadClicked,
  292. )
  293. }
  294. }
  295. Button(
  296. title = localize(MR.strings.action_delete),
  297. icon = Icons.Outlined.Delete,
  298. toConfirm = confirm[4],
  299. onLongClick = { onLongClickItem(4) },
  300. onClick = onDeleteClicked,
  301. )
  302. }
  303. }
  304. }
  305. }