mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Use Voyager on Downloads screen (#8640)
This commit is contained in:
		| @@ -7,19 +7,14 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem | ||||
| /** | ||||
|  * Adapter storing a list of downloads. | ||||
|  * | ||||
|  * @param context the context of the fragment containing this adapter. | ||||
|  * @param downloadItemListener Listener called when an item of the list is released. | ||||
|  */ | ||||
| class DownloadAdapter(controller: DownloadController) : FlexibleAdapter<AbstractFlexibleItem<*>>( | ||||
| class DownloadAdapter(val downloadItemListener: DownloadItemListener) : FlexibleAdapter<AbstractFlexibleItem<*>>( | ||||
|     null, | ||||
|     controller, | ||||
|     downloadItemListener, | ||||
|     true, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Listener called when an item of the list is released. | ||||
|      */ | ||||
|     val downloadItemListener: DownloadItemListener = controller | ||||
|  | ||||
|     override fun shouldMove(fromPosition: Int, toPosition: Int): Boolean { | ||||
|         // Don't let sub-items changing group | ||||
|         return getHeaderOf(getItem(fromPosition)) == getHeaderOf(getItem(toPosition)) | ||||
|   | ||||
| @@ -1,496 +1,15 @@ | ||||
| package eu.kanade.tachiyomi.ui.download | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import android.view.ViewGroup.MarginLayoutParams | ||||
| import androidx.compose.animation.AnimatedVisibility | ||||
| import androidx.compose.animation.fadeIn | ||||
| import androidx.compose.animation.fadeOut | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.navigationBarsPadding | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PlayArrow | ||||
| import androidx.compose.material.icons.outlined.Pause | ||||
| import androidx.compose.material3.DropdownMenuItem | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBarDefaults | ||||
| import androidx.compose.material3.rememberTopAppBarState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.geometry.Offset | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollSource | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.Velocity | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import androidx.core.view.ViewCompat | ||||
| import androidx.core.view.updateLayoutParams | ||||
| import androidx.core.view.updatePadding | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.ExtendedFloatingActionButton | ||||
| import eu.kanade.presentation.components.OverflowMenu | ||||
| import eu.kanade.presentation.components.Pill | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.databinding.DownloadListBinding | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlin.math.roundToInt | ||||
| import cafe.adriel.voyager.navigator.Navigator | ||||
| import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController | ||||
|  | ||||
| /** | ||||
|  * Controller that shows the currently active downloads. | ||||
|  */ | ||||
| class DownloadController : | ||||
|     FullComposeController<DownloadPresenter>(), | ||||
|     DownloadAdapter.DownloadItemListener { | ||||
|  | ||||
|     private lateinit var controllerBinding: DownloadListBinding | ||||
|  | ||||
|     /** | ||||
|      * Adapter containing the active downloads. | ||||
|      */ | ||||
|     private var adapter: DownloadAdapter? = null | ||||
|  | ||||
|     /** | ||||
|      * Map of subscriptions for active downloads. | ||||
|      */ | ||||
|     private val progressSubscriptions by lazy { mutableMapOf<Download, Subscription>() } | ||||
|  | ||||
|     override fun createPresenter() = DownloadPresenter() | ||||
|  | ||||
| class DownloadController : BasicFullComposeController() { | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         val context = LocalContext.current | ||||
|         val downloadList by presenter.state.collectAsState() | ||||
|         val downloadCount by remember { | ||||
|             derivedStateOf { downloadList.sumOf { it.subItems.size } } | ||||
|         } | ||||
|  | ||||
|         val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) | ||||
|         var fabExpanded by remember { mutableStateOf(true) } | ||||
|         val nestedScrollConnection = remember { | ||||
|             // All this lines just for fab state :/ | ||||
|             object : NestedScrollConnection { | ||||
|                 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { | ||||
|                     fabExpanded = available.y >= 0 | ||||
|                     return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) | ||||
|                 } | ||||
|  | ||||
|                 override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) | ||||
|                 } | ||||
|  | ||||
|                 override suspend fun onPreFling(available: Velocity): Velocity { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPreFling(available) | ||||
|                 } | ||||
|  | ||||
|                 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Scaffold( | ||||
|             topBar = { | ||||
|                 AppBar( | ||||
|                     titleContent = { | ||||
|                         Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                             Text( | ||||
|                                 text = stringResource(R.string.label_download_queue), | ||||
|                                 maxLines = 1, | ||||
|                                 modifier = Modifier.weight(1f, false), | ||||
|                                 overflow = TextOverflow.Ellipsis, | ||||
|                             ) | ||||
|                             if (downloadCount > 0) { | ||||
|                                 val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f | ||||
|                                 Pill( | ||||
|                                     text = "$downloadCount", | ||||
|                                     modifier = Modifier.padding(start = 4.dp), | ||||
|                                     color = MaterialTheme.colorScheme.onBackground | ||||
|                                         .copy(alpha = pillAlpha), | ||||
|                                     fontSize = 14.sp, | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     navigateUp = router::popCurrentController, | ||||
|                     actions = { | ||||
|                         if (downloadList.isNotEmpty()) { | ||||
|                             OverflowMenu { closeMenu -> | ||||
|                                 DropdownMenuItem( | ||||
|                                     text = { Text(text = stringResource(R.string.action_reorganize_by)) }, | ||||
|                                     children = { | ||||
|                                         DropdownMenuItem( | ||||
|                                             text = { Text(text = stringResource(R.string.action_order_by_upload_date)) }, | ||||
|                                             children = { | ||||
|                                                 DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_newest)) }, | ||||
|                                                     onClick = { | ||||
|                                                         reorderQueue( | ||||
|                                                             { it.download.chapter.date_upload }, | ||||
|                                                             true, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                                 DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_oldest)) }, | ||||
|                                                     onClick = { | ||||
|                                                         reorderQueue( | ||||
|                                                             { it.download.chapter.date_upload }, | ||||
|                                                             false, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                             }, | ||||
|                                         ) | ||||
|                                         DropdownMenuItem( | ||||
|                                             text = { Text(text = stringResource(R.string.action_order_by_chapter_number)) }, | ||||
|                                             children = { | ||||
|                                                 DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_asc)) }, | ||||
|                                                     onClick = { | ||||
|                                                         reorderQueue( | ||||
|                                                             { it.download.chapter.chapter_number }, | ||||
|                                                             false, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                                 DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_desc)) }, | ||||
|                                                     onClick = { | ||||
|                                                         reorderQueue( | ||||
|                                                             { it.download.chapter.chapter_number }, | ||||
|                                                             true, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                             }, | ||||
|                                         ) | ||||
|                                     }, | ||||
|                                 ) | ||||
|                                 DropdownMenuItem( | ||||
|                                     text = { Text(text = stringResource(R.string.action_cancel_all)) }, | ||||
|                                     onClick = { | ||||
|                                         presenter.clearQueue(context) | ||||
|                                         closeMenu() | ||||
|                                     }, | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     scrollBehavior = scrollBehavior, | ||||
|                 ) | ||||
|             }, | ||||
|             floatingActionButton = { | ||||
|                 AnimatedVisibility( | ||||
|                     visible = downloadList.isNotEmpty(), | ||||
|                     enter = fadeIn(), | ||||
|                     exit = fadeOut(), | ||||
|                 ) { | ||||
|                     val isRunning by DownloadService.isRunning.collectAsState() | ||||
|                     ExtendedFloatingActionButton( | ||||
|                         text = { | ||||
|                             val id = if (isRunning) { | ||||
|                                 R.string.action_pause | ||||
|                             } else { | ||||
|                                 R.string.action_resume | ||||
|                             } | ||||
|                             Text(text = stringResource(id)) | ||||
|                         }, | ||||
|                         icon = { | ||||
|                             val icon = if (isRunning) { | ||||
|                                 Icons.Outlined.Pause | ||||
|                             } else { | ||||
|                                 Icons.Filled.PlayArrow | ||||
|                             } | ||||
|                             Icon(imageVector = icon, contentDescription = null) | ||||
|                         }, | ||||
|                         onClick = { | ||||
|                             if (isRunning) { | ||||
|                                 DownloadService.stop(context) | ||||
|                                 presenter.pauseDownloads() | ||||
|                             } else { | ||||
|                                 DownloadService.start(context) | ||||
|                             } | ||||
|                         }, | ||||
|                         expanded = fabExpanded, | ||||
|                         modifier = Modifier.navigationBarsPadding(), | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) { contentPadding -> | ||||
|             if (downloadList.isEmpty()) { | ||||
|                 EmptyScreen( | ||||
|                     textResource = R.string.information_no_downloads, | ||||
|                     modifier = Modifier.padding(contentPadding), | ||||
|                 ) | ||||
|                 return@Scaffold | ||||
|             } | ||||
|             val density = LocalDensity.current | ||||
|             val layoutDirection = LocalLayoutDirection.current | ||||
|             val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } | ||||
|             val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() } | ||||
|             val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() } | ||||
|             val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() } | ||||
|  | ||||
|             Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) { | ||||
|                 AndroidView( | ||||
|                     factory = { context -> | ||||
|                         controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context)) | ||||
|                         adapter = DownloadAdapter(this@DownloadController) | ||||
|                         controllerBinding.recycler.adapter = adapter | ||||
|                         adapter?.isHandleDragEnabled = true | ||||
|                         adapter?.fastScroller = controllerBinding.fastScroller | ||||
|                         controllerBinding.recycler.layoutManager = LinearLayoutManager(context) | ||||
|  | ||||
|                         ViewCompat.setNestedScrollingEnabled(controllerBinding.root, true) | ||||
|  | ||||
|                         viewScope.launchUI { | ||||
|                             presenter.getDownloadStatusFlow() | ||||
|                                 .collect(this@DownloadController::onStatusChange) | ||||
|                         } | ||||
|                         viewScope.launchUI { | ||||
|                             presenter.getDownloadProgressFlow() | ||||
|                                 .collect(this@DownloadController::onUpdateDownloadedPages) | ||||
|                         } | ||||
|  | ||||
|                         controllerBinding.root | ||||
|                     }, | ||||
|                     update = { | ||||
|                         controllerBinding.recycler | ||||
|                             .updatePadding( | ||||
|                                 left = left, | ||||
|                                 top = top, | ||||
|                                 right = right, | ||||
|                                 bottom = bottom, | ||||
|                             ) | ||||
|  | ||||
|                         controllerBinding.fastScroller | ||||
|                             .updateLayoutParams<MarginLayoutParams> { | ||||
|                                 leftMargin = left | ||||
|                                 topMargin = top | ||||
|                                 rightMargin = right | ||||
|                                 bottomMargin = bottom | ||||
|                             } | ||||
|  | ||||
|                         adapter?.updateDataSet(downloadList) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView(view: View) { | ||||
|         for (subscription in progressSubscriptions.values) { | ||||
|             subscription.unsubscribe() | ||||
|         } | ||||
|         progressSubscriptions.clear() | ||||
|         adapter = null | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     private 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 }) | ||||
|         } | ||||
|         presenter.reorder(newDownloads) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the status of a download changes. | ||||
|      * | ||||
|      * @param download the download whose status has changed. | ||||
|      */ | ||||
|     private fun onStatusChange(download: Download) { | ||||
|         when (download.status) { | ||||
|             Download.State.DOWNLOADING -> { | ||||
|                 observeProgress(download) | ||||
|                 // Initial update of the downloaded pages | ||||
|                 onUpdateDownloadedPages(download) | ||||
|             } | ||||
|             Download.State.DOWNLOADED -> { | ||||
|                 unsubscribeProgress(download) | ||||
|                 onUpdateProgress(download) | ||||
|                 onUpdateDownloadedPages(download) | ||||
|             } | ||||
|             Download.State.ERROR -> unsubscribeProgress(download) | ||||
|             else -> { | ||||
|                 /* unused */ | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Observe the progress of a download and notify the view. | ||||
|      * | ||||
|      * @param download the download to observe its progress. | ||||
|      */ | ||||
|     private fun observeProgress(download: Download) { | ||||
|         val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) | ||||
|             // Get the sum of percentages for all the pages. | ||||
|             .flatMap { | ||||
|                 Observable.from(download.pages) | ||||
|                     .map(Page::progress) | ||||
|                     .reduce { x, y -> x + y } | ||||
|             } | ||||
|             // Keep only the latest emission to avoid backpressure. | ||||
|             .onBackpressureLatest() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe { progress -> | ||||
|                 // Update the view only if the progress has changed. | ||||
|                 if (download.totalProgress != progress) { | ||||
|                     download.totalProgress = progress | ||||
|                     onUpdateProgress(download) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         // Avoid leaking subscriptions | ||||
|         progressSubscriptions.remove(download)?.unsubscribe() | ||||
|  | ||||
|         progressSubscriptions[download] = subscription | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Unsubscribes the given download from the progress subscriptions. | ||||
|      * | ||||
|      * @param download the download to unsubscribe. | ||||
|      */ | ||||
|     private fun unsubscribeProgress(download: Download) { | ||||
|         progressSubscriptions.remove(download)?.unsubscribe() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      */ | ||||
|     private 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.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|             } | ||||
|         } | ||||
|         presenter.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 }) | ||||
|                     } | ||||
|                     presenter.reorder(newDownloads) | ||||
|                 } | ||||
|                 R.id.move_to_top_series -> { | ||||
|                     val (selectedSeries, otherSeries) = adapter?.currentItems | ||||
|                         ?.filterIsInstance<DownloadItem>() | ||||
|                         ?.map(DownloadItem::download) | ||||
|                         ?.partition { item.download.manga.id == it.manga.id } | ||||
|                         ?: Pair(emptyList(), emptyList()) | ||||
|                     presenter.reorder(selectedSeries + otherSeries) | ||||
|                 } | ||||
|                 R.id.cancel_download -> { | ||||
|                     presenter.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()) { | ||||
|                         presenter.cancel(allDownloadsForSeries) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Navigator(screen = DownloadQueueScreen) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,65 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.ui.download | ||||
|  | ||||
| import android.content.Context | ||||
| import android.os.Bundle | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class DownloadPresenter( | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
| ) : BasePresenter<DownloadController>() { | ||||
|  | ||||
|     private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>()) | ||||
|     val state = _state.asStateFlow() | ||||
|  | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
|  | ||||
|         presenterScope.launch { | ||||
|             downloadManager.queue.updates | ||||
|                 .catch { logcat(LogPriority.ERROR, it) } | ||||
|                 .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 } } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getDownloadStatusFlow() = downloadManager.queue.statusFlow() | ||||
|     fun getDownloadProgressFlow() = downloadManager.queue.progressFlow() | ||||
|  | ||||
|     fun pauseDownloads() { | ||||
|         downloadManager.pauseDownloads() | ||||
|     } | ||||
|  | ||||
|     fun clearQueue(context: Context) { | ||||
|         DownloadService.stop(context) | ||||
|         downloadManager.clearQueue() | ||||
|     } | ||||
|  | ||||
|     fun reorder(downloads: List<Download>) { | ||||
|         downloadManager.reorderQueue(downloads) | ||||
|     } | ||||
|  | ||||
|     fun cancel(downloads: List<Download>) { | ||||
|         downloadManager.cancelQueuedDownloads(downloads) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,294 @@ | ||||
| package eu.kanade.tachiyomi.ui.download | ||||
|  | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import androidx.compose.animation.AnimatedVisibility | ||||
| import androidx.compose.animation.fadeIn | ||||
| import androidx.compose.animation.fadeOut | ||||
| import androidx.compose.foundation.isSystemInDarkTheme | ||||
| import androidx.compose.foundation.layout.Box | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.navigationBarsPadding | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.filled.PlayArrow | ||||
| import androidx.compose.material.icons.outlined.Pause | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBarDefaults | ||||
| import androidx.compose.material3.rememberTopAppBarState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.geometry.Offset | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection | ||||
| import androidx.compose.ui.input.nestedscroll.NestedScrollSource | ||||
| import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.text.style.TextOverflow | ||||
| import androidx.compose.ui.unit.Velocity | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.sp | ||||
| import androidx.compose.ui.viewinterop.AndroidView | ||||
| import androidx.core.view.ViewCompat | ||||
| import androidx.core.view.updateLayoutParams | ||||
| import androidx.core.view.updatePadding | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import cafe.adriel.voyager.core.model.rememberScreenModel | ||||
| import cafe.adriel.voyager.core.screen.Screen | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.ExtendedFloatingActionButton | ||||
| import eu.kanade.presentation.components.OverflowMenu | ||||
| import eu.kanade.presentation.components.Pill | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.util.LocalRouter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.databinding.DownloadListBinding | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| object DownloadQueueScreen : Screen { | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         val context = LocalContext.current | ||||
|         val router = LocalRouter.currentOrThrow | ||||
|         val scope = rememberCoroutineScope() | ||||
|         val screenModel = rememberScreenModel { DownloadQueueScreenModel() } | ||||
|         val downloadList by screenModel.state.collectAsState() | ||||
|         val downloadCount by remember { | ||||
|             derivedStateOf { downloadList.sumOf { it.subItems.size } } | ||||
|         } | ||||
|  | ||||
|         val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) | ||||
|         var fabExpanded by remember { mutableStateOf(true) } | ||||
|         val nestedScrollConnection = remember { | ||||
|             // All this lines just for fab state :/ | ||||
|             object : NestedScrollConnection { | ||||
|                 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { | ||||
|                     fabExpanded = available.y >= 0 | ||||
|                     return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) | ||||
|                 } | ||||
|  | ||||
|                 override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) | ||||
|                 } | ||||
|  | ||||
|                 override suspend fun onPreFling(available: Velocity): Velocity { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPreFling(available) | ||||
|                 } | ||||
|  | ||||
|                 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { | ||||
|                     return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Scaffold( | ||||
|             topBar = { | ||||
|                 AppBar( | ||||
|                     titleContent = { | ||||
|                         Row(verticalAlignment = Alignment.CenterVertically) { | ||||
|                             Text( | ||||
|                                 text = stringResource(R.string.label_download_queue), | ||||
|                                 maxLines = 1, | ||||
|                                 modifier = Modifier.weight(1f, false), | ||||
|                                 overflow = TextOverflow.Ellipsis, | ||||
|                             ) | ||||
|                             if (downloadCount > 0) { | ||||
|                                 val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f | ||||
|                                 Pill( | ||||
|                                     text = "$downloadCount", | ||||
|                                     modifier = Modifier.padding(start = 4.dp), | ||||
|                                     color = MaterialTheme.colorScheme.onBackground | ||||
|                                         .copy(alpha = pillAlpha), | ||||
|                                     fontSize = 14.sp, | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     navigateUp = router::popCurrentController, | ||||
|                     actions = { | ||||
|                         if (downloadList.isNotEmpty()) { | ||||
|                             OverflowMenu { closeMenu -> | ||||
|                                 DropdownMenuItem( | ||||
|                                     text = { Text(text = stringResource(R.string.action_reorganize_by)) }, | ||||
|                                     children = { | ||||
|                                         DropdownMenuItem( | ||||
|                                             text = { Text(text = stringResource(R.string.action_order_by_upload_date)) }, | ||||
|                                             children = { | ||||
|                                                 androidx.compose.material3.DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_newest)) }, | ||||
|                                                     onClick = { | ||||
|                                                         screenModel.reorderQueue( | ||||
|                                                             { it.download.chapter.date_upload }, | ||||
|                                                             true, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                                 androidx.compose.material3.DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_oldest)) }, | ||||
|                                                     onClick = { | ||||
|                                                         screenModel.reorderQueue( | ||||
|                                                             { it.download.chapter.date_upload }, | ||||
|                                                             false, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                             }, | ||||
|                                         ) | ||||
|                                         DropdownMenuItem( | ||||
|                                             text = { Text(text = stringResource(R.string.action_order_by_chapter_number)) }, | ||||
|                                             children = { | ||||
|                                                 androidx.compose.material3.DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_asc)) }, | ||||
|                                                     onClick = { | ||||
|                                                         screenModel.reorderQueue( | ||||
|                                                             { it.download.chapter.chapter_number }, | ||||
|                                                             false, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                                 androidx.compose.material3.DropdownMenuItem( | ||||
|                                                     text = { Text(text = stringResource(R.string.action_desc)) }, | ||||
|                                                     onClick = { | ||||
|                                                         screenModel.reorderQueue( | ||||
|                                                             { it.download.chapter.chapter_number }, | ||||
|                                                             true, | ||||
|                                                         ) | ||||
|                                                         closeMenu() | ||||
|                                                     }, | ||||
|                                                 ) | ||||
|                                             }, | ||||
|                                         ) | ||||
|                                     }, | ||||
|                                 ) | ||||
|                                 androidx.compose.material3.DropdownMenuItem( | ||||
|                                     text = { Text(text = stringResource(R.string.action_cancel_all)) }, | ||||
|                                     onClick = { | ||||
|                                         screenModel.clearQueue(context) | ||||
|                                         closeMenu() | ||||
|                                     }, | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     scrollBehavior = scrollBehavior, | ||||
|                 ) | ||||
|             }, | ||||
|             floatingActionButton = { | ||||
|                 AnimatedVisibility( | ||||
|                     visible = downloadList.isNotEmpty(), | ||||
|                     enter = fadeIn(), | ||||
|                     exit = fadeOut(), | ||||
|                 ) { | ||||
|                     val isRunning by DownloadService.isRunning.collectAsState() | ||||
|                     ExtendedFloatingActionButton( | ||||
|                         text = { | ||||
|                             val id = if (isRunning) { | ||||
|                                 R.string.action_pause | ||||
|                             } else { | ||||
|                                 R.string.action_resume | ||||
|                             } | ||||
|                             Text(text = stringResource(id)) | ||||
|                         }, | ||||
|                         icon = { | ||||
|                             val icon = if (isRunning) { | ||||
|                                 Icons.Outlined.Pause | ||||
|                             } else { | ||||
|                                 Icons.Filled.PlayArrow | ||||
|                             } | ||||
|                             Icon(imageVector = icon, contentDescription = null) | ||||
|                         }, | ||||
|                         onClick = { | ||||
|                             if (isRunning) { | ||||
|                                 DownloadService.stop(context) | ||||
|                                 screenModel.pauseDownloads() | ||||
|                             } else { | ||||
|                                 DownloadService.start(context) | ||||
|                             } | ||||
|                         }, | ||||
|                         expanded = fabExpanded, | ||||
|                         modifier = Modifier.navigationBarsPadding(), | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) { contentPadding -> | ||||
|             if (downloadList.isEmpty()) { | ||||
|                 EmptyScreen( | ||||
|                     textResource = R.string.information_no_downloads, | ||||
|                     modifier = Modifier.padding(contentPadding), | ||||
|                 ) | ||||
|                 return@Scaffold | ||||
|             } | ||||
|             val density = LocalDensity.current | ||||
|             val layoutDirection = LocalLayoutDirection.current | ||||
|             val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() } | ||||
|             val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() } | ||||
|             val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() } | ||||
|             val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() } | ||||
|  | ||||
|             Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) { | ||||
|                 AndroidView( | ||||
|                     factory = { context -> | ||||
|                         screenModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context)) | ||||
|                         screenModel.adapter = DownloadAdapter(screenModel.listener) | ||||
|                         screenModel.controllerBinding.recycler.adapter = screenModel.adapter | ||||
|                         screenModel.adapter?.isHandleDragEnabled = true | ||||
|                         screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller | ||||
|                         screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(context) | ||||
|  | ||||
|                         ViewCompat.setNestedScrollingEnabled(screenModel.controllerBinding.root, true) | ||||
|  | ||||
|                         scope.launchUI { | ||||
|                             screenModel.getDownloadStatusFlow() | ||||
|                                 .collect(screenModel::onStatusChange) | ||||
|                         } | ||||
|                         scope.launchUI { | ||||
|                             screenModel.getDownloadProgressFlow() | ||||
|                                 .collect(screenModel::onUpdateDownloadedPages) | ||||
|                         } | ||||
|  | ||||
|                         screenModel.controllerBinding.root | ||||
|                     }, | ||||
|                     update = { | ||||
|                         screenModel.controllerBinding.recycler | ||||
|                             .updatePadding( | ||||
|                                 left = left, | ||||
|                                 top = top, | ||||
|                                 right = right, | ||||
|                                 bottom = bottom, | ||||
|                             ) | ||||
|  | ||||
|                         screenModel.controllerBinding.fastScroller | ||||
|                             .updateLayoutParams<ViewGroup.MarginLayoutParams> { | ||||
|                                 leftMargin = left | ||||
|                                 topMargin = top | ||||
|                                 rightMargin = right | ||||
|                                 bottomMargin = bottom | ||||
|                             } | ||||
|  | ||||
|                         screenModel.adapter?.updateDataSet(downloadList) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,265 @@ | ||||
| package eu.kanade.tachiyomi.ui.download | ||||
|  | ||||
| import android.content.Context | ||||
| import android.view.MenuItem | ||||
| import cafe.adriel.voyager.core.model.ScreenModel | ||||
| import cafe.adriel.voyager.core.model.coroutineScope | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.databinding.DownloadListBinding | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| 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 subscriptions for active downloads. | ||||
|      */ | ||||
|     val progressSubscriptions by lazy { mutableMapOf<Download, Subscription>() } | ||||
|  | ||||
|     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 -> { | ||||
|                         val (selectedSeries, otherSeries) = adapter?.currentItems | ||||
|                             ?.filterIsInstance<DownloadItem>() | ||||
|                             ?.map(DownloadItem::download) | ||||
|                             ?.partition { item.download.manga.id == it.manga.id } | ||||
|                             ?: Pair(emptyList(), emptyList()) | ||||
|                         reorder(selectedSeries + otherSeries) | ||||
|                     } | ||||
|                     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 { | ||||
|         coroutineScope.launch { | ||||
|             downloadManager.queue.updates | ||||
|                 .catch { logcat(LogPriority.ERROR, it) } | ||||
|                 .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 (subscription in progressSubscriptions.values) { | ||||
|             subscription.unsubscribe() | ||||
|         } | ||||
|         progressSubscriptions.clear() | ||||
|         adapter = null | ||||
|     } | ||||
|  | ||||
|     fun getDownloadStatusFlow() = downloadManager.queue.statusFlow() | ||||
|     fun getDownloadProgressFlow() = downloadManager.queue.progressFlow() | ||||
|  | ||||
|     fun pauseDownloads() { | ||||
|         downloadManager.pauseDownloads() | ||||
|     } | ||||
|  | ||||
|     fun clearQueue(context: Context) { | ||||
|         DownloadService.stop(context) | ||||
|         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 -> { | ||||
|                 observeProgress(download) | ||||
|                 // Initial update of the downloaded pages | ||||
|                 onUpdateDownloadedPages(download) | ||||
|             } | ||||
|             Download.State.DOWNLOADED -> { | ||||
|                 unsubscribeProgress(download) | ||||
|                 onUpdateProgress(download) | ||||
|                 onUpdateDownloadedPages(download) | ||||
|             } | ||||
|             Download.State.ERROR -> unsubscribeProgress(download) | ||||
|             else -> { | ||||
|                 /* unused */ | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Observe the progress of a download and notify the view. | ||||
|      * | ||||
|      * @param download the download to observe its progress. | ||||
|      */ | ||||
|     private fun observeProgress(download: Download) { | ||||
|         val subscription = Observable.interval(50, TimeUnit.MILLISECONDS) | ||||
|             // Get the sum of percentages for all the pages. | ||||
|             .flatMap { | ||||
|                 Observable.from(download.pages) | ||||
|                     .map(Page::progress) | ||||
|                     .reduce { x, y -> x + y } | ||||
|             } | ||||
|             // Keep only the latest emission to avoid backpressure. | ||||
|             .onBackpressureLatest() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe { progress -> | ||||
|                 // Update the view only if the progress has changed. | ||||
|                 if (download.totalProgress != progress) { | ||||
|                     download.totalProgress = progress | ||||
|                     onUpdateProgress(download) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         // Avoid leaking subscriptions | ||||
|         progressSubscriptions.remove(download)?.unsubscribe() | ||||
|  | ||||
|         progressSubscriptions[download] = subscription | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Unsubscribes the given download from the progress subscriptions. | ||||
|      * | ||||
|      * @param download the download to unsubscribe. | ||||
|      */ | ||||
|     private fun unsubscribeProgress(download: Download) { | ||||
|         progressSubscriptions.remove(download)?.unsubscribe() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user