ChapterDownloadIndicator.kt 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package eu.kanade.presentation.components
  2. import androidx.compose.animation.core.animateFloatAsState
  3. import androidx.compose.foundation.combinedClickable
  4. import androidx.compose.foundation.interaction.MutableInteractionSource
  5. import androidx.compose.foundation.layout.Box
  6. import androidx.compose.foundation.layout.padding
  7. import androidx.compose.foundation.layout.size
  8. import androidx.compose.material.icons.Icons
  9. import androidx.compose.material.icons.filled.CheckCircle
  10. import androidx.compose.material.icons.outlined.ArrowDownward
  11. import androidx.compose.material.icons.outlined.ErrorOutline
  12. import androidx.compose.material.ripple.rememberRipple
  13. import androidx.compose.material3.CircularProgressIndicator
  14. import androidx.compose.material3.DropdownMenuItem
  15. import androidx.compose.material3.Icon
  16. import androidx.compose.material3.MaterialTheme
  17. import androidx.compose.material3.ProgressIndicatorDefaults
  18. import androidx.compose.material3.Text
  19. import androidx.compose.runtime.Composable
  20. import androidx.compose.runtime.getValue
  21. import androidx.compose.runtime.mutableStateOf
  22. import androidx.compose.runtime.remember
  23. import androidx.compose.runtime.setValue
  24. import androidx.compose.ui.Alignment
  25. import androidx.compose.ui.Modifier
  26. import androidx.compose.ui.composed
  27. import androidx.compose.ui.graphics.Color
  28. import androidx.compose.ui.hapticfeedback.HapticFeedbackType
  29. import androidx.compose.ui.platform.LocalHapticFeedback
  30. import androidx.compose.ui.res.painterResource
  31. import androidx.compose.ui.res.stringResource
  32. import androidx.compose.ui.semantics.Role
  33. import androidx.compose.ui.unit.dp
  34. import eu.kanade.presentation.util.secondaryItemAlpha
  35. import eu.kanade.tachiyomi.R
  36. import eu.kanade.tachiyomi.data.download.model.Download
  37. enum class ChapterDownloadAction {
  38. START,
  39. START_NOW,
  40. CANCEL,
  41. DELETE,
  42. }
  43. @Composable
  44. fun ChapterDownloadIndicator(
  45. enabled: Boolean,
  46. modifier: Modifier = Modifier,
  47. downloadStateProvider: () -> Download.State,
  48. downloadProgressProvider: () -> Int,
  49. onClick: (ChapterDownloadAction) -> Unit,
  50. ) {
  51. when (val downloadState = downloadStateProvider()) {
  52. Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
  53. enabled = enabled,
  54. modifier = modifier,
  55. onClick = onClick,
  56. )
  57. Download.State.QUEUE, Download.State.DOWNLOADING -> DownloadingIndicator(
  58. enabled = enabled,
  59. modifier = modifier,
  60. downloadState = downloadState,
  61. downloadProgressProvider = downloadProgressProvider,
  62. onClick = onClick,
  63. )
  64. Download.State.DOWNLOADED -> DownloadedIndicator(
  65. enabled = enabled,
  66. modifier = modifier,
  67. onClick = onClick,
  68. )
  69. Download.State.ERROR -> ErrorIndicator(
  70. enabled = enabled,
  71. modifier = modifier,
  72. onClick = onClick,
  73. )
  74. }
  75. }
  76. @Composable
  77. private fun NotDownloadedIndicator(
  78. enabled: Boolean,
  79. modifier: Modifier = Modifier,
  80. onClick: (ChapterDownloadAction) -> Unit,
  81. ) {
  82. Box(
  83. modifier = modifier
  84. .size(IconButtonTokens.StateLayerSize)
  85. .commonClickable(
  86. enabled = enabled,
  87. onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
  88. onClick = { onClick(ChapterDownloadAction.START) },
  89. )
  90. .secondaryItemAlpha(),
  91. contentAlignment = Alignment.Center,
  92. ) {
  93. Icon(
  94. painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
  95. contentDescription = stringResource(R.string.manga_download),
  96. modifier = Modifier.size(IndicatorSize),
  97. tint = MaterialTheme.colorScheme.onSurfaceVariant,
  98. )
  99. }
  100. }
  101. @Composable
  102. private fun DownloadingIndicator(
  103. enabled: Boolean,
  104. modifier: Modifier = Modifier,
  105. downloadState: Download.State,
  106. downloadProgressProvider: () -> Int,
  107. onClick: (ChapterDownloadAction) -> Unit,
  108. ) {
  109. var isMenuExpanded by remember { mutableStateOf(false) }
  110. Box(
  111. modifier = modifier
  112. .size(IconButtonTokens.StateLayerSize)
  113. .commonClickable(
  114. enabled = enabled,
  115. onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
  116. onClick = { isMenuExpanded = true },
  117. ),
  118. contentAlignment = Alignment.Center,
  119. ) {
  120. val arrowColor: Color
  121. val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant
  122. val downloadProgress = downloadProgressProvider()
  123. val indeterminate = downloadState == Download.State.QUEUE ||
  124. (downloadState == Download.State.DOWNLOADING && downloadProgress == 0)
  125. if (indeterminate) {
  126. arrowColor = strokeColor
  127. CircularProgressIndicator(
  128. modifier = IndicatorModifier,
  129. color = strokeColor,
  130. strokeWidth = IndicatorStrokeWidth,
  131. )
  132. } else {
  133. val animatedProgress by animateFloatAsState(
  134. targetValue = downloadProgress / 100f,
  135. animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
  136. )
  137. arrowColor = if (animatedProgress < 0.5f) {
  138. strokeColor
  139. } else {
  140. MaterialTheme.colorScheme.background
  141. }
  142. CircularProgressIndicator(
  143. progress = animatedProgress,
  144. modifier = IndicatorModifier,
  145. color = strokeColor,
  146. strokeWidth = IndicatorSize / 2,
  147. )
  148. }
  149. DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
  150. DropdownMenuItem(
  151. text = { Text(text = stringResource(R.string.action_start_downloading_now)) },
  152. onClick = {
  153. onClick(ChapterDownloadAction.START_NOW)
  154. isMenuExpanded = false
  155. },
  156. )
  157. DropdownMenuItem(
  158. text = { Text(text = stringResource(R.string.action_cancel)) },
  159. onClick = {
  160. onClick(ChapterDownloadAction.CANCEL)
  161. isMenuExpanded = false
  162. },
  163. )
  164. }
  165. Icon(
  166. imageVector = Icons.Outlined.ArrowDownward,
  167. contentDescription = null,
  168. modifier = ArrowModifier,
  169. tint = arrowColor,
  170. )
  171. }
  172. }
  173. @Composable
  174. private fun DownloadedIndicator(
  175. enabled: Boolean,
  176. modifier: Modifier = Modifier,
  177. onClick: (ChapterDownloadAction) -> Unit,
  178. ) {
  179. var isMenuExpanded by remember { mutableStateOf(false) }
  180. Box(
  181. modifier = modifier
  182. .size(IconButtonTokens.StateLayerSize)
  183. .commonClickable(
  184. enabled = enabled,
  185. onLongClick = { isMenuExpanded = true },
  186. onClick = { isMenuExpanded = true },
  187. ),
  188. contentAlignment = Alignment.Center,
  189. ) {
  190. Icon(
  191. imageVector = Icons.Filled.CheckCircle,
  192. contentDescription = null,
  193. modifier = Modifier.size(IndicatorSize),
  194. tint = MaterialTheme.colorScheme.onSurfaceVariant,
  195. )
  196. DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
  197. DropdownMenuItem(
  198. text = { Text(text = stringResource(R.string.action_delete)) },
  199. onClick = {
  200. onClick(ChapterDownloadAction.DELETE)
  201. isMenuExpanded = false
  202. },
  203. )
  204. }
  205. }
  206. }
  207. @Composable
  208. private fun ErrorIndicator(
  209. enabled: Boolean,
  210. modifier: Modifier = Modifier,
  211. onClick: (ChapterDownloadAction) -> Unit,
  212. ) {
  213. Box(
  214. modifier = modifier
  215. .size(IconButtonTokens.StateLayerSize)
  216. .commonClickable(
  217. enabled = enabled,
  218. onLongClick = { onClick(ChapterDownloadAction.START) },
  219. onClick = { onClick(ChapterDownloadAction.START) },
  220. ),
  221. contentAlignment = Alignment.Center,
  222. ) {
  223. Icon(
  224. imageVector = Icons.Outlined.ErrorOutline,
  225. contentDescription = stringResource(R.string.chapter_error),
  226. modifier = Modifier.size(IndicatorSize),
  227. tint = MaterialTheme.colorScheme.error,
  228. )
  229. }
  230. }
  231. private fun Modifier.commonClickable(
  232. enabled: Boolean,
  233. onLongClick: () -> Unit,
  234. onClick: () -> Unit,
  235. ) = composed {
  236. val haptic = LocalHapticFeedback.current
  237. this.combinedClickable(
  238. enabled = enabled,
  239. onLongClick = {
  240. onLongClick()
  241. haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  242. },
  243. onClick = onClick,
  244. role = Role.Button,
  245. interactionSource = remember { MutableInteractionSource() },
  246. indication = rememberRipple(
  247. bounded = false,
  248. radius = IconButtonTokens.StateLayerSize / 2,
  249. ),
  250. )
  251. }
  252. private val IndicatorSize = 26.dp
  253. private val IndicatorPadding = 2.dp
  254. // To match composable parameter name when used later
  255. private val IndicatorStrokeWidth = IndicatorPadding
  256. private val IndicatorModifier = Modifier
  257. .size(IndicatorSize)
  258. .padding(IndicatorPadding)
  259. private val ArrowModifier = Modifier
  260. .size(IndicatorSize - 7.dp)