123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- package eu.kanade.tachiyomi.ui.download
- import android.view.MenuItem
- import cafe.adriel.voyager.core.model.ScreenModel
- import cafe.adriel.voyager.core.model.screenModelScope
- import eu.kanade.tachiyomi.R
- import eu.kanade.tachiyomi.data.download.DownloadManager
- import eu.kanade.tachiyomi.data.download.model.Download
- import eu.kanade.tachiyomi.databinding.DownloadListBinding
- import eu.kanade.tachiyomi.source.model.Page
- import kotlinx.coroutines.Job
- import kotlinx.coroutines.delay
- import kotlinx.coroutines.flow.MutableStateFlow
- import kotlinx.coroutines.flow.asStateFlow
- import kotlinx.coroutines.flow.collectLatest
- import kotlinx.coroutines.flow.combine
- import kotlinx.coroutines.flow.debounce
- import kotlinx.coroutines.flow.distinctUntilChanged
- import kotlinx.coroutines.flow.map
- import kotlinx.coroutines.flow.update
- import kotlinx.coroutines.launch
- import uy.kohesive.injekt.Injekt
- import uy.kohesive.injekt.api.get
- class DownloadQueueScreenModel(
- private val downloadManager: DownloadManager = Injekt.get(),
- ) : ScreenModel {
- private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
- val state = _state.asStateFlow()
- lateinit var controllerBinding: DownloadListBinding
- /**
- * Adapter containing the active downloads.
- */
- var adapter: DownloadAdapter? = null
- /**
- * Map of jobs for active downloads.
- */
- private val progressJobs = mutableMapOf<Download, Job>()
- val listener = object : DownloadAdapter.DownloadItemListener {
- /**
- * Called when an item is released from a drag.
- *
- * @param position The position of the released item.
- */
- override fun onItemReleased(position: Int) {
- val adapter = adapter ?: return
- val downloads = adapter.headerItems.flatMap { header ->
- adapter.getSectionItems(header).map { item ->
- (item as DownloadItem).download
- }
- }
- reorder(downloads)
- }
- /**
- * Called when the menu item of a download is pressed
- *
- * @param position The position of the item
- * @param menuItem The menu Item pressed
- */
- override fun onMenuItemClick(position: Int, menuItem: MenuItem) {
- val item = adapter?.getItem(position) ?: return
- if (item is DownloadItem) {
- when (menuItem.itemId) {
- R.id.move_to_top, R.id.move_to_bottom -> {
- val headerItems = adapter?.headerItems ?: return
- val newDownloads = mutableListOf<Download>()
- headerItems.forEach { headerItem ->
- headerItem as DownloadHeaderItem
- if (headerItem == item.header) {
- headerItem.removeSubItem(item)
- if (menuItem.itemId == R.id.move_to_top) {
- headerItem.addSubItem(0, item)
- } else {
- headerItem.addSubItem(item)
- }
- }
- newDownloads.addAll(headerItem.subItems.map { it.download })
- }
- reorder(newDownloads)
- }
- R.id.move_to_top_series, R.id.move_to_bottom_series -> {
- val (selectedSeries, otherSeries) = adapter?.currentItems
- ?.filterIsInstance<DownloadItem>()
- ?.map(DownloadItem::download)
- ?.partition { item.download.manga.id == it.manga.id }
- ?: Pair(emptyList(), emptyList())
- if (menuItem.itemId == R.id.move_to_top_series) {
- reorder(selectedSeries + otherSeries)
- } else {
- reorder(otherSeries + selectedSeries)
- }
- }
- R.id.cancel_download -> {
- cancel(listOf(item.download))
- }
- R.id.cancel_series -> {
- val allDownloadsForSeries = adapter?.currentItems
- ?.filterIsInstance<DownloadItem>()
- ?.filter { item.download.manga.id == it.download.manga.id }
- ?.map(DownloadItem::download)
- if (!allDownloadsForSeries.isNullOrEmpty()) {
- cancel(allDownloadsForSeries)
- }
- }
- }
- }
- }
- }
- init {
- screenModelScope.launch {
- downloadManager.queueState
- .map { downloads ->
- downloads
- .groupBy { it.source }
- .map { entry ->
- DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
- addSubItems(0, entry.value.map { DownloadItem(it, this) })
- }
- }
- }
- .collect { newList -> _state.update { newList } }
- }
- }
- override fun onDispose() {
- for (job in progressJobs.values) {
- job.cancel()
- }
- progressJobs.clear()
- adapter = null
- }
- val isDownloaderRunning
- get() = downloadManager.isDownloaderRunning
- fun getDownloadStatusFlow() = downloadManager.statusFlow()
- fun getDownloadProgressFlow() = downloadManager.progressFlow()
- fun startDownloads() {
- downloadManager.startDownloads()
- }
- fun pauseDownloads() {
- downloadManager.pauseDownloads()
- }
- fun clearQueue() {
- downloadManager.clearQueue()
- }
- fun reorder(downloads: List<Download>) {
- downloadManager.reorderQueue(downloads)
- }
- fun cancel(downloads: List<Download>) {
- downloadManager.cancelQueuedDownloads(downloads)
- }
- fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
- val adapter = adapter ?: return
- val newDownloads = mutableListOf<Download>()
- adapter.headerItems.forEach { headerItem ->
- headerItem as DownloadHeaderItem
- headerItem.subItems = headerItem.subItems.sortedBy(selector).toMutableList().apply {
- if (reverse) {
- reverse()
- }
- }
- newDownloads.addAll(headerItem.subItems.map { it.download })
- }
- reorder(newDownloads)
- }
- /**
- * Called when the status of a download changes.
- *
- * @param download the download whose status has changed.
- */
- fun onStatusChange(download: Download) {
- when (download.status) {
- Download.State.DOWNLOADING -> {
- launchProgressJob(download)
- // Initial update of the downloaded pages
- onUpdateDownloadedPages(download)
- }
- Download.State.DOWNLOADED -> {
- cancelProgressJob(download)
- onUpdateProgress(download)
- onUpdateDownloadedPages(download)
- }
- Download.State.ERROR -> cancelProgressJob(download)
- else -> {
- /* unused */
- }
- }
- }
- /**
- * Observe the progress of a download and notify the view.
- *
- * @param download the download to observe its progress.
- */
- private fun launchProgressJob(download: Download) {
- val job = screenModelScope.launch {
- while (download.pages == null) {
- delay(50)
- }
- val progressFlows = download.pages!!.map(Page::progressFlow)
- combine(progressFlows, Array<Int>::sum)
- .distinctUntilChanged()
- .debounce(50)
- .collectLatest {
- onUpdateProgress(download)
- }
- }
- // Avoid leaking jobs
- progressJobs.remove(download)?.cancel()
- progressJobs[download] = job
- }
- /**
- * Unsubscribes the given download from the progress subscriptions.
- *
- * @param download the download to unsubscribe.
- */
- private fun cancelProgressJob(download: Download) {
- progressJobs.remove(download)?.cancel()
- }
- /**
- * Called when the progress of a download changes.
- *
- * @param download the download whose progress has changed.
- */
- private fun onUpdateProgress(download: Download) {
- getHolder(download)?.notifyProgress()
- }
- /**
- * Called when a page of a download is downloaded.
- *
- * @param download the download whose page has been downloaded.
- */
- fun onUpdateDownloadedPages(download: Download) {
- getHolder(download)?.notifyDownloadedPages()
- }
- /**
- * Returns the holder for the given download.
- *
- * @param download the download to find.
- * @return the holder of the download or null if it's not bound.
- */
- private fun getHolder(download: Download): DownloadHolder? {
- return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder
- }
- }
|