UpdatesUiItem.kt 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. package eu.kanade.presentation.updates
  2. import androidx.compose.foundation.background
  3. import androidx.compose.foundation.combinedClickable
  4. import androidx.compose.foundation.layout.Column
  5. import androidx.compose.foundation.layout.Row
  6. import androidx.compose.foundation.layout.Spacer
  7. import androidx.compose.foundation.layout.fillMaxHeight
  8. import androidx.compose.foundation.layout.height
  9. import androidx.compose.foundation.layout.padding
  10. import androidx.compose.foundation.layout.sizeIn
  11. import androidx.compose.foundation.layout.width
  12. import androidx.compose.foundation.lazy.LazyListScope
  13. import androidx.compose.foundation.lazy.items
  14. import androidx.compose.material.icons.Icons
  15. import androidx.compose.material.icons.filled.Bookmark
  16. import androidx.compose.material3.Icon
  17. import androidx.compose.material3.MaterialTheme
  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.draw.alpha
  27. import androidx.compose.ui.graphics.Color
  28. import androidx.compose.ui.platform.LocalDensity
  29. import androidx.compose.ui.res.stringResource
  30. import androidx.compose.ui.text.style.TextOverflow
  31. import androidx.compose.ui.unit.dp
  32. import eu.kanade.domain.updates.model.UpdatesWithRelations
  33. import eu.kanade.presentation.components.ChapterDownloadAction
  34. import eu.kanade.presentation.components.ChapterDownloadIndicator
  35. import eu.kanade.presentation.components.MangaCover
  36. import eu.kanade.presentation.components.RelativeDateHeader
  37. import eu.kanade.presentation.util.ReadItemAlpha
  38. import eu.kanade.presentation.util.horizontalPadding
  39. import eu.kanade.tachiyomi.R
  40. import eu.kanade.tachiyomi.data.download.model.Download
  41. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
  42. import java.text.DateFormat
  43. fun LazyListScope.updatesUiItems(
  44. uiModels: List<UpdatesUiModel>,
  45. itemUiModels: List<UpdatesUiModel.Item>,
  46. selected: MutableList<UpdatesUiModel.Item>,
  47. selectedPositions: Array<Int>,
  48. onClickCover: (UpdatesItem) -> Unit,
  49. onClickUpdate: (UpdatesItem) -> Unit,
  50. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  51. relativeTime: Int,
  52. dateFormat: DateFormat,
  53. ) {
  54. items(
  55. items = uiModels,
  56. contentType = {
  57. when (it) {
  58. is UpdatesUiModel.Header -> "header"
  59. is UpdatesUiModel.Item -> "item"
  60. }
  61. },
  62. key = {
  63. when (it) {
  64. is UpdatesUiModel.Header -> it.hashCode()
  65. is UpdatesUiModel.Item -> it.item.update.chapterId
  66. }
  67. },
  68. ) { item ->
  69. when (item) {
  70. is UpdatesUiModel.Header -> {
  71. RelativeDateHeader(
  72. modifier = Modifier.animateItemPlacement(),
  73. date = item.date,
  74. relativeTime = relativeTime,
  75. dateFormat = dateFormat,
  76. )
  77. }
  78. is UpdatesUiModel.Item -> {
  79. val value = item.item
  80. val update = value.update
  81. UpdatesUiItem(
  82. modifier = Modifier.animateItemPlacement(),
  83. update = update,
  84. selected = selected.contains(item),
  85. onClick = {
  86. onUpdatesItemClick(
  87. updatesItem = item,
  88. selected = selected,
  89. updates = itemUiModels,
  90. selectedPositions = selectedPositions,
  91. onUpdateClicked = onClickUpdate,
  92. )
  93. },
  94. onLongClick = {
  95. onUpdatesItemLongClick(
  96. updatesItem = item,
  97. selected = selected,
  98. updates = itemUiModels,
  99. selectedPositions = selectedPositions,
  100. )
  101. },
  102. onClickCover = { if (selected.size == 0) onClickCover(value) },
  103. onDownloadChapter = {
  104. if (selected.size == 0) onDownloadChapter(listOf(value), it)
  105. },
  106. downloadStateProvider = value.downloadStateProvider,
  107. downloadProgressProvider = value.downloadProgressProvider,
  108. )
  109. }
  110. }
  111. }
  112. }
  113. @Composable
  114. fun UpdatesUiItem(
  115. modifier: Modifier,
  116. update: UpdatesWithRelations,
  117. selected: Boolean,
  118. onClick: () -> Unit,
  119. onLongClick: () -> Unit,
  120. onClickCover: () -> Unit,
  121. onDownloadChapter: (ChapterDownloadAction) -> Unit,
  122. // Download Indicator
  123. downloadStateProvider: () -> Download.State,
  124. downloadProgressProvider: () -> Int,
  125. ) {
  126. Row(
  127. modifier = modifier
  128. .background(if (selected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent)
  129. .combinedClickable(
  130. onClick = onClick,
  131. onLongClick = onLongClick,
  132. )
  133. .height(56.dp)
  134. .padding(horizontal = horizontalPadding),
  135. verticalAlignment = Alignment.CenterVertically,
  136. ) {
  137. MangaCover.Square(
  138. modifier = Modifier
  139. .padding(vertical = 6.dp)
  140. .fillMaxHeight(),
  141. data = update.coverData,
  142. onClick = onClickCover,
  143. )
  144. Column(
  145. modifier = Modifier
  146. .padding(horizontal = horizontalPadding)
  147. .weight(1f),
  148. ) {
  149. val bookmark = remember(update.bookmark) { update.bookmark }
  150. val read = remember(update.read) { update.read }
  151. val textAlpha = remember(read) { if (read) ReadItemAlpha else 1f }
  152. val secondaryTextColor = if (bookmark && !read) {
  153. MaterialTheme.colorScheme.primary
  154. } else {
  155. MaterialTheme.colorScheme.onSurface
  156. }
  157. Text(
  158. text = update.mangaTitle,
  159. maxLines = 1,
  160. style = MaterialTheme.typography.bodyMedium,
  161. overflow = TextOverflow.Ellipsis,
  162. modifier = Modifier.alpha(textAlpha),
  163. )
  164. Row(verticalAlignment = Alignment.CenterVertically) {
  165. var textHeight by remember { mutableStateOf(0) }
  166. if (bookmark) {
  167. Icon(
  168. imageVector = Icons.Default.Bookmark,
  169. contentDescription = stringResource(R.string.action_filter_bookmarked),
  170. modifier = Modifier
  171. .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
  172. tint = MaterialTheme.colorScheme.primary,
  173. )
  174. Spacer(modifier = Modifier.width(2.dp))
  175. }
  176. Text(
  177. text = update.chapterName,
  178. maxLines = 1,
  179. style = MaterialTheme.typography.bodySmall
  180. .copy(color = secondaryTextColor),
  181. overflow = TextOverflow.Ellipsis,
  182. onTextLayout = { textHeight = it.size.height },
  183. modifier = Modifier.alpha(textAlpha),
  184. )
  185. }
  186. }
  187. ChapterDownloadIndicator(
  188. modifier = Modifier.padding(start = 4.dp),
  189. downloadStateProvider = downloadStateProvider,
  190. downloadProgressProvider = downloadProgressProvider,
  191. onClick = onDownloadChapter,
  192. )
  193. }
  194. }
  195. private fun onUpdatesItemLongClick(
  196. updatesItem: UpdatesUiModel.Item,
  197. selected: MutableList<UpdatesUiModel.Item>,
  198. updates: List<UpdatesUiModel.Item>,
  199. selectedPositions: Array<Int>,
  200. ): Boolean {
  201. if (!selected.contains(updatesItem)) {
  202. val selectedIndex = updates.indexOf(updatesItem)
  203. if (selected.isEmpty()) {
  204. selected.add(updatesItem)
  205. selectedPositions[0] = selectedIndex
  206. selectedPositions[1] = selectedIndex
  207. return true
  208. }
  209. // Try to select the items in-between when possible
  210. val range: IntRange
  211. if (selectedIndex < selectedPositions[0]) {
  212. range = selectedIndex until selectedPositions[0]
  213. selectedPositions[0] = selectedIndex
  214. } else if (selectedIndex > selectedPositions[1]) {
  215. range = (selectedPositions[1] + 1)..selectedIndex
  216. selectedPositions[1] = selectedIndex
  217. } else {
  218. // Just select itself
  219. range = selectedIndex..selectedIndex
  220. }
  221. range.forEach {
  222. val toAdd = updates[it]
  223. if (!selected.contains(toAdd)) {
  224. selected.add(toAdd)
  225. }
  226. }
  227. return true
  228. }
  229. return false
  230. }
  231. private fun onUpdatesItemClick(
  232. updatesItem: UpdatesUiModel.Item,
  233. selected: MutableList<UpdatesUiModel.Item>,
  234. updates: List<UpdatesUiModel.Item>,
  235. selectedPositions: Array<Int>,
  236. onUpdateClicked: (UpdatesItem) -> Unit,
  237. ) {
  238. val selectedIndex = updates.indexOf(updatesItem)
  239. when {
  240. selected.contains(updatesItem) -> {
  241. val removedIndex = updates.indexOf(updatesItem)
  242. selected.remove(updatesItem)
  243. if (removedIndex == selectedPositions[0]) {
  244. selectedPositions[0] = updates.indexOfFirst { selected.contains(it) }
  245. } else if (removedIndex == selectedPositions[1]) {
  246. selectedPositions[1] = updates.indexOfLast { selected.contains(it) }
  247. }
  248. }
  249. selected.isNotEmpty() -> {
  250. if (selectedIndex < selectedPositions[0]) {
  251. selectedPositions[0] = selectedIndex
  252. } else if (selectedIndex > selectedPositions[1]) {
  253. selectedPositions[1] = selectedIndex
  254. }
  255. selected.add(updatesItem)
  256. }
  257. else -> onUpdateClicked(updatesItem.item)
  258. }
  259. }