UpdatesScreen.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. package eu.kanade.presentation.updates
  2. import androidx.activity.compose.BackHandler
  3. import androidx.compose.foundation.layout.PaddingValues
  4. import androidx.compose.foundation.layout.calculateEndPadding
  5. import androidx.compose.foundation.layout.fillMaxHeight
  6. import androidx.compose.foundation.layout.fillMaxWidth
  7. import androidx.compose.foundation.lazy.rememberLazyListState
  8. import androidx.compose.material.icons.Icons
  9. import androidx.compose.material.icons.filled.FlipToBack
  10. import androidx.compose.material.icons.filled.Refresh
  11. import androidx.compose.material.icons.filled.SelectAll
  12. import androidx.compose.material3.Icon
  13. import androidx.compose.material3.IconButton
  14. import androidx.compose.material3.TopAppBarScrollBehavior
  15. import androidx.compose.runtime.Composable
  16. import androidx.compose.runtime.LaunchedEffect
  17. import androidx.compose.runtime.getValue
  18. import androidx.compose.runtime.mutableStateOf
  19. import androidx.compose.runtime.remember
  20. import androidx.compose.runtime.rememberCoroutineScope
  21. import androidx.compose.runtime.setValue
  22. import androidx.compose.ui.Modifier
  23. import androidx.compose.ui.platform.LocalContext
  24. import androidx.compose.ui.platform.LocalLayoutDirection
  25. import androidx.compose.ui.res.stringResource
  26. import com.google.accompanist.swiperefresh.SwipeRefresh
  27. import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
  28. import eu.kanade.presentation.components.AppBar
  29. import eu.kanade.presentation.components.ChapterDownloadAction
  30. import eu.kanade.presentation.components.EmptyScreen
  31. import eu.kanade.presentation.components.LazyColumn
  32. import eu.kanade.presentation.components.LoadingScreen
  33. import eu.kanade.presentation.components.MangaBottomActionMenu
  34. import eu.kanade.presentation.components.Scaffold
  35. import eu.kanade.presentation.components.SwipeRefreshIndicator
  36. import eu.kanade.presentation.components.VerticalFastScroller
  37. import eu.kanade.presentation.util.plus
  38. import eu.kanade.tachiyomi.R
  39. import eu.kanade.tachiyomi.data.download.model.Download
  40. import eu.kanade.tachiyomi.data.library.LibraryUpdateService
  41. import eu.kanade.tachiyomi.ui.reader.ReaderActivity
  42. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesItem
  43. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter
  44. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Dialog
  45. import eu.kanade.tachiyomi.ui.recent.updates.UpdatesPresenter.Event
  46. import eu.kanade.tachiyomi.util.system.toast
  47. import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView.Companion.bottomNavPadding
  48. import kotlinx.coroutines.delay
  49. import kotlinx.coroutines.flow.collectLatest
  50. import kotlinx.coroutines.launch
  51. import java.util.Date
  52. @Composable
  53. fun UpdateScreen(
  54. presenter: UpdatesPresenter,
  55. onClickCover: (UpdatesItem) -> Unit,
  56. onBackClicked: () -> Unit,
  57. ) {
  58. val internalOnBackPressed = {
  59. if (presenter.selectionMode) {
  60. presenter.toggleAllSelection(false)
  61. } else {
  62. onBackClicked()
  63. }
  64. }
  65. BackHandler(onBack = internalOnBackPressed)
  66. val context = LocalContext.current
  67. val onUpdateLibrary = {
  68. val started = LibraryUpdateService.start(context)
  69. context.toast(if (started) R.string.updating_library else R.string.update_already_running)
  70. started
  71. }
  72. Scaffold(
  73. topBar = { scrollBehavior ->
  74. UpdatesAppBar(
  75. incognitoMode = presenter.isIncognitoMode,
  76. downloadedOnlyMode = presenter.isDownloadOnly,
  77. onUpdateLibrary = { onUpdateLibrary() },
  78. actionModeCounter = presenter.selected.size,
  79. onSelectAll = { presenter.toggleAllSelection(true) },
  80. onInvertSelection = { presenter.invertSelection() },
  81. onCancelActionMode = { presenter.toggleAllSelection(false) },
  82. scrollBehavior = scrollBehavior,
  83. )
  84. },
  85. bottomBar = {
  86. UpdatesBottomBar(
  87. selected = presenter.selected,
  88. onDownloadChapter = presenter::downloadChapters,
  89. onMultiBookmarkClicked = presenter::bookmarkUpdates,
  90. onMultiMarkAsReadClicked = presenter::markUpdatesRead,
  91. onMultiDeleteClicked = {
  92. presenter.dialog = Dialog.DeleteConfirmation(it)
  93. },
  94. )
  95. },
  96. ) { contentPadding ->
  97. when {
  98. presenter.isLoading -> LoadingScreen()
  99. presenter.uiModels.isEmpty() -> EmptyScreen(textResource = R.string.information_no_recent)
  100. else -> {
  101. UpdateScreenContent(
  102. presenter = presenter,
  103. contentPadding = contentPadding,
  104. onUpdateLibrary = onUpdateLibrary,
  105. onClickCover = onClickCover,
  106. )
  107. }
  108. }
  109. }
  110. }
  111. @Composable
  112. private fun UpdateScreenContent(
  113. presenter: UpdatesPresenter,
  114. contentPadding: PaddingValues,
  115. onUpdateLibrary: () -> Boolean,
  116. onClickCover: (UpdatesItem) -> Unit,
  117. ) {
  118. val context = LocalContext.current
  119. val updatesListState = rememberLazyListState()
  120. // During selection mode bottom nav is not visible
  121. val contentPaddingWithNavBar = contentPadding + bottomNavPadding
  122. val scope = rememberCoroutineScope()
  123. var isRefreshing by remember { mutableStateOf(false) }
  124. SwipeRefresh(
  125. state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
  126. onRefresh = {
  127. val started = onUpdateLibrary()
  128. if (!started) return@SwipeRefresh
  129. scope.launch {
  130. // Fake refresh status but hide it after a second as it's a long running task
  131. isRefreshing = true
  132. delay(1000)
  133. isRefreshing = false
  134. }
  135. },
  136. swipeEnabled = presenter.selectionMode.not(),
  137. indicatorPadding = contentPaddingWithNavBar,
  138. indicator = { s, trigger ->
  139. SwipeRefreshIndicator(
  140. state = s,
  141. refreshTriggerDistance = trigger,
  142. )
  143. },
  144. ) {
  145. if (presenter.uiModels.isEmpty()) {
  146. EmptyScreen(textResource = R.string.information_no_recent)
  147. } else {
  148. VerticalFastScroller(
  149. listState = updatesListState,
  150. topContentPadding = contentPaddingWithNavBar.calculateTopPadding(),
  151. endContentPadding = contentPaddingWithNavBar.calculateEndPadding(LocalLayoutDirection.current),
  152. ) {
  153. LazyColumn(
  154. modifier = Modifier.fillMaxHeight(),
  155. state = updatesListState,
  156. contentPadding = contentPaddingWithNavBar,
  157. ) {
  158. if (presenter.lastUpdated > 0L) {
  159. updatesLastUpdatedItem(presenter.lastUpdated)
  160. }
  161. updatesUiItems(
  162. uiModels = presenter.uiModels,
  163. selectionMode = presenter.selectionMode,
  164. onUpdateSelected = presenter::toggleSelection,
  165. onClickCover = onClickCover,
  166. onClickUpdate = {
  167. val intent = ReaderActivity.newIntent(context, it.update.mangaId, it.update.chapterId)
  168. context.startActivity(intent)
  169. },
  170. onDownloadChapter = presenter::downloadChapters,
  171. relativeTime = presenter.relativeTime,
  172. dateFormat = presenter.dateFormat,
  173. )
  174. }
  175. }
  176. }
  177. }
  178. val onDismissDialog = { presenter.dialog = null }
  179. when (val dialog = presenter.dialog) {
  180. is Dialog.DeleteConfirmation -> {
  181. UpdatesDeleteConfirmationDialog(
  182. onDismissRequest = onDismissDialog,
  183. onConfirm = {
  184. presenter.toggleAllSelection(false)
  185. presenter.deleteChapters(dialog.toDelete)
  186. },
  187. )
  188. }
  189. null -> {}
  190. }
  191. LaunchedEffect(Unit) {
  192. presenter.events.collectLatest { event ->
  193. when (event) {
  194. Event.InternalError -> context.toast(R.string.internal_error)
  195. }
  196. }
  197. }
  198. }
  199. @Composable
  200. private fun UpdatesAppBar(
  201. modifier: Modifier = Modifier,
  202. incognitoMode: Boolean,
  203. downloadedOnlyMode: Boolean,
  204. onUpdateLibrary: () -> Unit,
  205. // For action mode
  206. actionModeCounter: Int,
  207. onSelectAll: () -> Unit,
  208. onInvertSelection: () -> Unit,
  209. onCancelActionMode: () -> Unit,
  210. scrollBehavior: TopAppBarScrollBehavior,
  211. ) {
  212. AppBar(
  213. modifier = modifier,
  214. title = stringResource(R.string.label_recent_updates),
  215. actions = {
  216. IconButton(onClick = onUpdateLibrary) {
  217. Icon(
  218. imageVector = Icons.Default.Refresh,
  219. contentDescription = stringResource(R.string.action_update_library),
  220. )
  221. }
  222. },
  223. actionModeCounter = actionModeCounter,
  224. onCancelActionMode = onCancelActionMode,
  225. actionModeActions = {
  226. IconButton(onClick = onSelectAll) {
  227. Icon(
  228. imageVector = Icons.Default.SelectAll,
  229. contentDescription = stringResource(R.string.action_select_all),
  230. )
  231. }
  232. IconButton(onClick = onInvertSelection) {
  233. Icon(
  234. imageVector = Icons.Default.FlipToBack,
  235. contentDescription = stringResource(R.string.action_select_inverse),
  236. )
  237. }
  238. },
  239. downloadedOnlyMode = downloadedOnlyMode,
  240. incognitoMode = incognitoMode,
  241. scrollBehavior = scrollBehavior,
  242. )
  243. }
  244. @Composable
  245. private fun UpdatesBottomBar(
  246. selected: List<UpdatesItem>,
  247. onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
  248. onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
  249. onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
  250. onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
  251. ) {
  252. MangaBottomActionMenu(
  253. visible = selected.isNotEmpty(),
  254. modifier = Modifier.fillMaxWidth(),
  255. onBookmarkClicked = {
  256. onMultiBookmarkClicked.invoke(selected, true)
  257. }.takeIf { selected.any { !it.update.bookmark } },
  258. onRemoveBookmarkClicked = {
  259. onMultiBookmarkClicked.invoke(selected, false)
  260. }.takeIf { selected.all { it.update.bookmark } },
  261. onMarkAsReadClicked = {
  262. onMultiMarkAsReadClicked(selected, true)
  263. }.takeIf { selected.any { !it.update.read } },
  264. onMarkAsUnreadClicked = {
  265. onMultiMarkAsReadClicked(selected, false)
  266. }.takeIf { selected.any { it.update.read } },
  267. onDownloadClicked = {
  268. onDownloadChapter(selected, ChapterDownloadAction.START)
  269. }.takeIf {
  270. selected.any { it.downloadStateProvider() != Download.State.DOWNLOADED }
  271. },
  272. onDeleteClicked = {
  273. onMultiDeleteClicked(selected)
  274. }.takeIf { selected.any { it.downloadStateProvider() == Download.State.DOWNLOADED } },
  275. )
  276. }
  277. sealed class UpdatesUiModel {
  278. data class Header(val date: Date) : UpdatesUiModel()
  279. data class Item(val item: UpdatesItem) : UpdatesUiModel()
  280. }