UpdatesScreen.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. package eu.kanade.presentation.updates
  2. import androidx.activity.compose.BackHandler
  3. import androidx.compose.foundation.layout.WindowInsets
  4. import androidx.compose.foundation.layout.WindowInsetsSides
  5. import androidx.compose.foundation.layout.asPaddingValues
  6. import androidx.compose.foundation.layout.calculateEndPadding
  7. import androidx.compose.foundation.layout.fillMaxHeight
  8. import androidx.compose.foundation.layout.fillMaxWidth
  9. import androidx.compose.foundation.layout.navigationBars
  10. import androidx.compose.foundation.layout.only
  11. import androidx.compose.foundation.layout.padding
  12. import androidx.compose.foundation.layout.systemBars
  13. import androidx.compose.foundation.lazy.LazyColumn
  14. import androidx.compose.foundation.lazy.rememberLazyListState
  15. import androidx.compose.material.icons.Icons
  16. import androidx.compose.material.icons.filled.FlipToBack
  17. import androidx.compose.material.icons.filled.Refresh
  18. import androidx.compose.material.icons.filled.SelectAll
  19. import androidx.compose.material3.Icon
  20. import androidx.compose.material3.IconButton
  21. import androidx.compose.runtime.Composable
  22. import androidx.compose.runtime.remember
  23. import androidx.compose.runtime.toMutableStateList
  24. import androidx.compose.ui.Modifier
  25. import androidx.compose.ui.platform.LocalLayoutDirection
  26. import androidx.compose.ui.res.stringResource
  27. import com.google.accompanist.swiperefresh.SwipeRefresh
  28. import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
  29. import eu.kanade.presentation.components.AppBar
  30. import eu.kanade.presentation.components.ChapterDownloadAction
  31. import eu.kanade.presentation.components.EmptyScreen
  32. import eu.kanade.presentation.components.MangaBottomActionMenu
  33. import eu.kanade.presentation.components.Scaffold
  34. import eu.kanade.presentation.components.SwipeRefreshIndicator
  35. import eu.kanade.presentation.components.VerticalFastScroller
  36. import eu.kanade.presentation.util.NavBarVisibility
  37. import eu.kanade.presentation.util.isScrollingDown
  38. import eu.kanade.presentation.util.isScrollingUp
  39. import eu.kanade.presentation.util.plus
  40. import eu.kanade.tachiyomi.R
  41. import eu.kanade.tachiyomi.data.download.model.Download
  42. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  43. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
  44. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesState
  45. import uy.kohesive.injekt.Injekt
  46. import uy.kohesive.injekt.api.get
  47. import java.text.DateFormat
  48. import java.util.Date
  49. @Composable
  50. fun UpdateScreen(
  51. state: UpdatesState.Success,
  52. onClickCover: (UpdatesItem) -> Unit,
  53. onClickUpdate: (UpdatesItem) -> Unit,
  54. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  55. onUpdateLibrary: () -> Unit,
  56. onBackClicked: () -> Unit,
  57. toggleNavBarVisibility: (NavBarVisibility) -> Unit,
  58. // For bottom action menu
  59. onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
  60. onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
  61. onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
  62. // Miscellaneous
  63. preferences: PreferencesHelper = Injekt.get(),
  64. ) {
  65. val updatesListState = rememberLazyListState()
  66. val insetPaddingValue = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
  67. val relativeTime: Int = remember { preferences.relativeTime().get() }
  68. val dateFormat: DateFormat = remember { preferences.dateFormat() }
  69. val uiModels = remember(state) {
  70. state.uiModels
  71. }
  72. val itemUiModels = remember(uiModels) {
  73. uiModels.filterIsInstance<UpdatesUiModel.Item>()
  74. }
  75. // To prevent selection from getting removed during an update to a item in list
  76. val updateIdList = remember(itemUiModels) {
  77. itemUiModels.map { it.item.update.chapterId }
  78. }
  79. val selected = remember(updateIdList) {
  80. emptyList<UpdatesUiModel.Item>().toMutableStateList()
  81. }
  82. // First and last selected index in list
  83. val selectedPositions = remember(uiModels) { arrayOf(-1, -1) }
  84. when {
  85. selected.isEmpty() &&
  86. updatesListState.isScrollingUp() -> toggleNavBarVisibility(NavBarVisibility.SHOW)
  87. selected.isNotEmpty() ||
  88. updatesListState.isScrollingDown() -> toggleNavBarVisibility(NavBarVisibility.HIDE)
  89. }
  90. val internalOnBackPressed = {
  91. if (selected.isNotEmpty()) {
  92. selected.clear()
  93. } else {
  94. onBackClicked()
  95. }
  96. }
  97. BackHandler(onBack = internalOnBackPressed)
  98. Scaffold(
  99. modifier = Modifier
  100. .padding(insetPaddingValue),
  101. topBar = {
  102. UpdatesAppBar(
  103. selected = selected,
  104. incognitoMode = state.isIncognitoMode,
  105. downloadedOnlyMode = state.isDownloadedOnlyMode,
  106. onUpdateLibrary = onUpdateLibrary,
  107. actionModeCounter = selected.size,
  108. onSelectAll = {
  109. selected.clear()
  110. selected.addAll(itemUiModels)
  111. },
  112. onInvertSelection = {
  113. val toSelect = itemUiModels - selected
  114. selected.clear()
  115. selected.addAll(toSelect)
  116. },
  117. )
  118. },
  119. bottomBar = {
  120. UpdatesBottomBar(
  121. selected = selected,
  122. onDownloadChapter = onDownloadChapter,
  123. onMultiBookmarkClicked = onMultiBookmarkClicked,
  124. onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
  125. onMultiDeleteClicked = onMultiDeleteClicked,
  126. )
  127. },
  128. ) { contentPadding ->
  129. val contentPaddingWithNavBar = contentPadding +
  130. WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
  131. SwipeRefresh(
  132. state = rememberSwipeRefreshState(state.showSwipeRefreshIndicator),
  133. onRefresh = onUpdateLibrary,
  134. indicatorPadding = contentPaddingWithNavBar,
  135. indicator = { s, trigger ->
  136. SwipeRefreshIndicator(
  137. state = s,
  138. refreshTriggerDistance = trigger,
  139. )
  140. },
  141. ) {
  142. if (uiModels.isEmpty()) {
  143. EmptyScreen(textResource = R.string.information_no_recent)
  144. } else {
  145. VerticalFastScroller(
  146. listState = updatesListState,
  147. topContentPadding = contentPaddingWithNavBar.calculateTopPadding(),
  148. endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current),
  149. ) {
  150. LazyColumn(
  151. modifier = Modifier.fillMaxHeight(),
  152. state = updatesListState,
  153. contentPadding = contentPaddingWithNavBar,
  154. ) {
  155. updatesUiItems(
  156. uiModels = uiModels,
  157. itemUiModels = itemUiModels,
  158. selected = selected,
  159. selectedPositions = selectedPositions,
  160. onClickCover = onClickCover,
  161. onClickUpdate = onClickUpdate,
  162. onDownloadChapter = onDownloadChapter,
  163. relativeTime = relativeTime,
  164. dateFormat = dateFormat,
  165. )
  166. }
  167. }
  168. }
  169. }
  170. }
  171. }
  172. @Composable
  173. fun UpdatesAppBar(
  174. modifier: Modifier = Modifier,
  175. selected: MutableList<UpdatesUiModel.Item>,
  176. incognitoMode: Boolean,
  177. downloadedOnlyMode: Boolean,
  178. onUpdateLibrary: () -> Unit,
  179. // For action mode
  180. actionModeCounter: Int,
  181. onSelectAll: () -> Unit,
  182. onInvertSelection: () -> Unit,
  183. ) {
  184. AppBar(
  185. modifier = modifier,
  186. title = stringResource(R.string.label_recent_updates),
  187. actions = {
  188. IconButton(onClick = onUpdateLibrary) {
  189. Icon(
  190. imageVector = Icons.Default.Refresh,
  191. contentDescription = stringResource(R.string.action_update_library),
  192. )
  193. }
  194. },
  195. actionModeCounter = actionModeCounter,
  196. onCancelActionMode = { selected.clear() },
  197. actionModeActions = {
  198. IconButton(onClick = onSelectAll) {
  199. Icon(
  200. imageVector = Icons.Default.SelectAll,
  201. contentDescription = stringResource(R.string.action_select_all),
  202. )
  203. }
  204. IconButton(onClick = onInvertSelection) {
  205. Icon(
  206. imageVector = Icons.Default.FlipToBack,
  207. contentDescription = stringResource(R.string.action_select_inverse),
  208. )
  209. }
  210. },
  211. downloadedOnlyMode = downloadedOnlyMode,
  212. incognitoMode = incognitoMode,
  213. )
  214. }
  215. @Composable
  216. fun UpdatesBottomBar(
  217. selected: MutableList<UpdatesUiModel.Item>,
  218. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  219. onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
  220. onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
  221. onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
  222. ) {
  223. MangaBottomActionMenu(
  224. visible = selected.isNotEmpty(),
  225. modifier = Modifier.fillMaxWidth(),
  226. onBookmarkClicked = {
  227. onMultiBookmarkClicked.invoke(selected.map { it.item }, true)
  228. selected.clear()
  229. }.takeIf { selected.any { !it.item.update.bookmark } },
  230. onRemoveBookmarkClicked = {
  231. onMultiBookmarkClicked.invoke(selected.map { it.item }, false)
  232. selected.clear()
  233. }.takeIf { selected.all { it.item.update.bookmark } },
  234. onMarkAsReadClicked = {
  235. onMultiMarkAsReadClicked(selected.map { it.item }, true)
  236. selected.clear()
  237. }.takeIf { selected.any { !it.item.update.read } },
  238. onMarkAsUnreadClicked = {
  239. onMultiMarkAsReadClicked(selected.map { it.item }, false)
  240. selected.clear()
  241. }.takeIf { selected.any { it.item.update.read } },
  242. onDownloadClicked = {
  243. onDownloadChapter(selected.map { it.item }, ChapterDownloadAction.START)
  244. selected.clear()
  245. }.takeIf {
  246. selected.any { it.item.downloadStateProvider() != Download.State.DOWNLOADED }
  247. },
  248. onDeleteClicked = {
  249. onMultiDeleteClicked(selected.map { it.item })
  250. selected.clear()
  251. }.takeIf { selected.any { it.item.downloadStateProvider() == Download.State.DOWNLOADED } },
  252. )
  253. }
  254. sealed class UpdatesUiModel {
  255. data class Header(val date: Date) : UpdatesUiModel()
  256. data class Item(val item: UpdatesItem) : UpdatesUiModel()
  257. }