DownloadQueueScreenModel.kt 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. package eu.kanade.tachiyomi.ui.download
  2. import android.view.MenuItem
  3. import cafe.adriel.voyager.core.model.ScreenModel
  4. import cafe.adriel.voyager.core.model.screenModelScope
  5. import eu.kanade.tachiyomi.R
  6. import eu.kanade.tachiyomi.data.download.DownloadManager
  7. import eu.kanade.tachiyomi.data.download.model.Download
  8. import eu.kanade.tachiyomi.databinding.DownloadListBinding
  9. import eu.kanade.tachiyomi.source.model.Page
  10. import kotlinx.coroutines.Job
  11. import kotlinx.coroutines.delay
  12. import kotlinx.coroutines.flow.MutableStateFlow
  13. import kotlinx.coroutines.flow.asStateFlow
  14. import kotlinx.coroutines.flow.collectLatest
  15. import kotlinx.coroutines.flow.combine
  16. import kotlinx.coroutines.flow.debounce
  17. import kotlinx.coroutines.flow.distinctUntilChanged
  18. import kotlinx.coroutines.flow.map
  19. import kotlinx.coroutines.flow.update
  20. import kotlinx.coroutines.launch
  21. import uy.kohesive.injekt.Injekt
  22. import uy.kohesive.injekt.api.get
  23. class DownloadQueueScreenModel(
  24. private val downloadManager: DownloadManager = Injekt.get(),
  25. ) : ScreenModel {
  26. private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
  27. val state = _state.asStateFlow()
  28. lateinit var controllerBinding: DownloadListBinding
  29. /**
  30. * Adapter containing the active downloads.
  31. */
  32. var adapter: DownloadAdapter? = null
  33. /**
  34. * Map of jobs for active downloads.
  35. */
  36. private val progressJobs = mutableMapOf<Download, Job>()
  37. val listener = object : DownloadAdapter.DownloadItemListener {
  38. /**
  39. * Called when an item is released from a drag.
  40. *
  41. * @param position The position of the released item.
  42. */
  43. override fun onItemReleased(position: Int) {
  44. val adapter = adapter ?: return
  45. val downloads = adapter.headerItems.flatMap { header ->
  46. adapter.getSectionItems(header).map { item ->
  47. (item as DownloadItem).download
  48. }
  49. }
  50. reorder(downloads)
  51. }
  52. /**
  53. * Called when the menu item of a download is pressed
  54. *
  55. * @param position The position of the item
  56. * @param menuItem The menu Item pressed
  57. */
  58. override fun onMenuItemClick(position: Int, menuItem: MenuItem) {
  59. val item = adapter?.getItem(position) ?: return
  60. if (item is DownloadItem) {
  61. when (menuItem.itemId) {
  62. R.id.move_to_top, R.id.move_to_bottom -> {
  63. val headerItems = adapter?.headerItems ?: return
  64. val newDownloads = mutableListOf<Download>()
  65. headerItems.forEach { headerItem ->
  66. headerItem as DownloadHeaderItem
  67. if (headerItem == item.header) {
  68. headerItem.removeSubItem(item)
  69. if (menuItem.itemId == R.id.move_to_top) {
  70. headerItem.addSubItem(0, item)
  71. } else {
  72. headerItem.addSubItem(item)
  73. }
  74. }
  75. newDownloads.addAll(headerItem.subItems.map { it.download })
  76. }
  77. reorder(newDownloads)
  78. }
  79. R.id.move_to_top_series, R.id.move_to_bottom_series -> {
  80. val (selectedSeries, otherSeries) = adapter?.currentItems
  81. ?.filterIsInstance<DownloadItem>()
  82. ?.map(DownloadItem::download)
  83. ?.partition { item.download.manga.id == it.manga.id }
  84. ?: Pair(emptyList(), emptyList())
  85. if (menuItem.itemId == R.id.move_to_top_series) {
  86. reorder(selectedSeries + otherSeries)
  87. } else {
  88. reorder(otherSeries + selectedSeries)
  89. }
  90. }
  91. R.id.cancel_download -> {
  92. cancel(listOf(item.download))
  93. }
  94. R.id.cancel_series -> {
  95. val allDownloadsForSeries = adapter?.currentItems
  96. ?.filterIsInstance<DownloadItem>()
  97. ?.filter { item.download.manga.id == it.download.manga.id }
  98. ?.map(DownloadItem::download)
  99. if (!allDownloadsForSeries.isNullOrEmpty()) {
  100. cancel(allDownloadsForSeries)
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }
  107. init {
  108. screenModelScope.launch {
  109. downloadManager.queueState
  110. .map { downloads ->
  111. downloads
  112. .groupBy { it.source }
  113. .map { entry ->
  114. DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
  115. addSubItems(0, entry.value.map { DownloadItem(it, this) })
  116. }
  117. }
  118. }
  119. .collect { newList -> _state.update { newList } }
  120. }
  121. }
  122. override fun onDispose() {
  123. for (job in progressJobs.values) {
  124. job.cancel()
  125. }
  126. progressJobs.clear()
  127. adapter = null
  128. }
  129. val isDownloaderRunning
  130. get() = downloadManager.isDownloaderRunning
  131. fun getDownloadStatusFlow() = downloadManager.statusFlow()
  132. fun getDownloadProgressFlow() = downloadManager.progressFlow()
  133. fun startDownloads() {
  134. downloadManager.startDownloads()
  135. }
  136. fun pauseDownloads() {
  137. downloadManager.pauseDownloads()
  138. }
  139. fun clearQueue() {
  140. downloadManager.clearQueue()
  141. }
  142. fun reorder(downloads: List<Download>) {
  143. downloadManager.reorderQueue(downloads)
  144. }
  145. fun cancel(downloads: List<Download>) {
  146. downloadManager.cancelQueuedDownloads(downloads)
  147. }
  148. fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
  149. val adapter = adapter ?: return
  150. val newDownloads = mutableListOf<Download>()
  151. adapter.headerItems.forEach { headerItem ->
  152. headerItem as DownloadHeaderItem
  153. headerItem.subItems = headerItem.subItems.sortedBy(selector).toMutableList().apply {
  154. if (reverse) {
  155. reverse()
  156. }
  157. }
  158. newDownloads.addAll(headerItem.subItems.map { it.download })
  159. }
  160. reorder(newDownloads)
  161. }
  162. /**
  163. * Called when the status of a download changes.
  164. *
  165. * @param download the download whose status has changed.
  166. */
  167. fun onStatusChange(download: Download) {
  168. when (download.status) {
  169. Download.State.DOWNLOADING -> {
  170. launchProgressJob(download)
  171. // Initial update of the downloaded pages
  172. onUpdateDownloadedPages(download)
  173. }
  174. Download.State.DOWNLOADED -> {
  175. cancelProgressJob(download)
  176. onUpdateProgress(download)
  177. onUpdateDownloadedPages(download)
  178. }
  179. Download.State.ERROR -> cancelProgressJob(download)
  180. else -> {
  181. /* unused */
  182. }
  183. }
  184. }
  185. /**
  186. * Observe the progress of a download and notify the view.
  187. *
  188. * @param download the download to observe its progress.
  189. */
  190. private fun launchProgressJob(download: Download) {
  191. val job = screenModelScope.launch {
  192. while (download.pages == null) {
  193. delay(50)
  194. }
  195. val progressFlows = download.pages!!.map(Page::progressFlow)
  196. combine(progressFlows, Array<Int>::sum)
  197. .distinctUntilChanged()
  198. .debounce(50)
  199. .collectLatest {
  200. onUpdateProgress(download)
  201. }
  202. }
  203. // Avoid leaking jobs
  204. progressJobs.remove(download)?.cancel()
  205. progressJobs[download] = job
  206. }
  207. /**
  208. * Unsubscribes the given download from the progress subscriptions.
  209. *
  210. * @param download the download to unsubscribe.
  211. */
  212. private fun cancelProgressJob(download: Download) {
  213. progressJobs.remove(download)?.cancel()
  214. }
  215. /**
  216. * Called when the progress of a download changes.
  217. *
  218. * @param download the download whose progress has changed.
  219. */
  220. private fun onUpdateProgress(download: Download) {
  221. getHolder(download)?.notifyProgress()
  222. }
  223. /**
  224. * Called when a page of a download is downloaded.
  225. *
  226. * @param download the download whose page has been downloaded.
  227. */
  228. fun onUpdateDownloadedPages(download: Download) {
  229. getHolder(download)?.notifyDownloadedPages()
  230. }
  231. /**
  232. * Returns the holder for the given download.
  233. *
  234. * @param download the download to find.
  235. * @return the holder of the download or null if it's not bound.
  236. */
  237. private fun getHolder(download: Download): DownloadHolder? {
  238. return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder
  239. }
  240. }