UpdatesScreen.kt 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package eu.kanade.presentation.updates
  2. import androidx.activity.compose.BackHandler
  3. import androidx.compose.foundation.layout.fillMaxWidth
  4. import androidx.compose.foundation.layout.padding
  5. import androidx.compose.material.icons.Icons
  6. import androidx.compose.material.icons.outlined.FlipToBack
  7. import androidx.compose.material.icons.outlined.Refresh
  8. import androidx.compose.material.icons.outlined.SelectAll
  9. import androidx.compose.material3.Icon
  10. import androidx.compose.material3.IconButton
  11. import androidx.compose.material3.SnackbarHost
  12. import androidx.compose.material3.SnackbarHostState
  13. import androidx.compose.material3.TopAppBarScrollBehavior
  14. import androidx.compose.runtime.Composable
  15. import androidx.compose.runtime.getValue
  16. import androidx.compose.runtime.mutableStateOf
  17. import androidx.compose.runtime.remember
  18. import androidx.compose.runtime.rememberCoroutineScope
  19. import androidx.compose.runtime.setValue
  20. import androidx.compose.ui.Modifier
  21. import androidx.compose.ui.platform.LocalContext
  22. import androidx.compose.ui.res.stringResource
  23. import androidx.compose.ui.util.fastAll
  24. import androidx.compose.ui.util.fastAny
  25. import eu.kanade.presentation.components.AppBar
  26. import eu.kanade.presentation.manga.components.ChapterDownloadAction
  27. import eu.kanade.presentation.manga.components.MangaBottomActionMenu
  28. import eu.kanade.tachiyomi.R
  29. import eu.kanade.tachiyomi.data.download.model.Download
  30. import eu.kanade.tachiyomi.ui.updates.UpdatesItem
  31. import eu.kanade.tachiyomi.ui.updates.UpdatesState
  32. import kotlinx.coroutines.delay
  33. import kotlinx.coroutines.launch
  34. import tachiyomi.presentation.core.components.FastScrollLazyColumn
  35. import tachiyomi.presentation.core.components.material.PullRefresh
  36. import tachiyomi.presentation.core.components.material.Scaffold
  37. import tachiyomi.presentation.core.screens.EmptyScreen
  38. import tachiyomi.presentation.core.screens.LoadingScreen
  39. import kotlin.time.Duration.Companion.seconds
  40. @Composable
  41. fun UpdateScreen(
  42. state: UpdatesState,
  43. snackbarHostState: SnackbarHostState,
  44. lastUpdated: Long,
  45. relativeTime: Int,
  46. onClickCover: (UpdatesItem) -> Unit,
  47. onSelectAll: (Boolean) -> Unit,
  48. onInvertSelection: () -> Unit,
  49. onUpdateLibrary: () -> Boolean,
  50. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  51. onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
  52. onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
  53. onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
  54. onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
  55. onOpenChapter: (UpdatesItem) -> Unit,
  56. ) {
  57. BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
  58. val context = LocalContext.current
  59. Scaffold(
  60. topBar = { scrollBehavior ->
  61. UpdatesAppBar(
  62. onUpdateLibrary = { onUpdateLibrary() },
  63. actionModeCounter = state.selected.size,
  64. onSelectAll = { onSelectAll(true) },
  65. onInvertSelection = { onInvertSelection() },
  66. onCancelActionMode = { onSelectAll(false) },
  67. scrollBehavior = scrollBehavior,
  68. )
  69. },
  70. bottomBar = {
  71. UpdatesBottomBar(
  72. selected = state.selected,
  73. onDownloadChapter = onDownloadChapter,
  74. onMultiBookmarkClicked = onMultiBookmarkClicked,
  75. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  76. onMultiDeleteClicked = onMultiDeleteClicked,
  77. )
  78. },
  79. snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
  80. ) { contentPadding ->
  81. when {
  82. state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
  83. state.items.isEmpty() -> EmptyScreen(
  84. textResource = R.string.information_no_recent,
  85. modifier = Modifier.padding(contentPadding),
  86. )
  87. else -> {
  88. val scope = rememberCoroutineScope()
  89. var isRefreshing by remember { mutableStateOf(false) }
  90. PullRefresh(
  91. refreshing = isRefreshing,
  92. onRefresh = {
  93. val started = onUpdateLibrary()
  94. if (!started) return@PullRefresh
  95. scope.launch {
  96. // Fake refresh status but hide it after a second as it's a long running task
  97. isRefreshing = true
  98. delay(1.seconds)
  99. isRefreshing = false
  100. }
  101. },
  102. enabled = !state.selectionMode,
  103. indicatorPadding = contentPadding,
  104. ) {
  105. FastScrollLazyColumn(
  106. contentPadding = contentPadding,
  107. ) {
  108. if (lastUpdated > 0L) {
  109. updatesLastUpdatedItem(lastUpdated)
  110. }
  111. updatesUiItems(
  112. uiModels = state.getUiModel(context, relativeTime),
  113. selectionMode = state.selectionMode,
  114. onUpdateSelected = onUpdateSelected,
  115. onClickCover = onClickCover,
  116. onClickUpdate = onOpenChapter,
  117. onDownloadChapter = onDownloadChapter,
  118. )
  119. }
  120. }
  121. }
  122. }
  123. }
  124. }
  125. @Composable
  126. private fun UpdatesAppBar(
  127. modifier: Modifier = Modifier,
  128. onUpdateLibrary: () -> Unit,
  129. // For action mode
  130. actionModeCounter: Int,
  131. onSelectAll: () -> Unit,
  132. onInvertSelection: () -> Unit,
  133. onCancelActionMode: () -> Unit,
  134. scrollBehavior: TopAppBarScrollBehavior,
  135. ) {
  136. AppBar(
  137. modifier = modifier,
  138. title = stringResource(R.string.label_recent_updates),
  139. actions = {
  140. IconButton(onClick = onUpdateLibrary) {
  141. Icon(
  142. imageVector = Icons.Outlined.Refresh,
  143. contentDescription = stringResource(R.string.action_update_library),
  144. )
  145. }
  146. },
  147. actionModeCounter = actionModeCounter,
  148. onCancelActionMode = onCancelActionMode,
  149. actionModeActions = {
  150. IconButton(onClick = onSelectAll) {
  151. Icon(
  152. imageVector = Icons.Outlined.SelectAll,
  153. contentDescription = stringResource(R.string.action_select_all),
  154. )
  155. }
  156. IconButton(onClick = onInvertSelection) {
  157. Icon(
  158. imageVector = Icons.Outlined.FlipToBack,
  159. contentDescription = stringResource(R.string.action_select_inverse),
  160. )
  161. }
  162. },
  163. scrollBehavior = scrollBehavior,
  164. )
  165. }
  166. @Composable
  167. private fun UpdatesBottomBar(
  168. selected: List<UpdatesItem>,
  169. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  170. onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
  171. onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
  172. onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
  173. ) {
  174. MangaBottomActionMenu(
  175. visible = selected.isNotEmpty(),
  176. modifier = Modifier.fillMaxWidth(),
  177. onBookmarkClicked = {
  178. onMultiBookmarkClicked.invoke(selected, true)
  179. }.takeIf { selected.fastAny { !it.update.bookmark } },
  180. onRemoveBookmarkClicked = {
  181. onMultiBookmarkClicked.invoke(selected, false)
  182. }.takeIf { selected.fastAll { it.update.bookmark } },
  183. onMarkAsReadClicked = {
  184. onMultiMarkAsReadClicked(selected, true)
  185. }.takeIf { selected.fastAny { !it.update.read } },
  186. onMarkAsUnreadClicked = {
  187. onMultiMarkAsReadClicked(selected, false)
  188. }.takeIf { selected.fastAny { it.update.read || it.update.lastPageRead > 0L } },
  189. onDownloadClicked = {
  190. onDownloadChapter(selected, ChapterDownloadAction.START)
  191. }.takeIf {
  192. selected.fastAny { it.downloadStateProvider() != Download.State.DOWNLOADED }
  193. },
  194. onDeleteClicked = {
  195. onMultiDeleteClicked(selected)
  196. }.takeIf { selected.fastAny { it.downloadStateProvider() == Download.State.DOWNLOADED } },
  197. )
  198. }
  199. sealed class UpdatesUiModel {
  200. data class Header(val date: String) : UpdatesUiModel()
  201. data class Item(val item: UpdatesItem) : UpdatesUiModel()
  202. }