UpdatesScreen.kt 8.5 KB

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