mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	DownloadController: Partial Compose conversion (#7969)
Item list is not changed as currently there is no fitting Compose component to replace the drag-drop behavior.
This commit is contained in:
		@@ -73,6 +73,7 @@ android {
 | 
			
		||||
            signingConfig = debugType.signingConfig
 | 
			
		||||
            versionNameSuffix = debugType.versionNameSuffix
 | 
			
		||||
            applicationIdSuffix = debugType.applicationIdSuffix
 | 
			
		||||
            matchingFallbacks.add("release")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -252,6 +253,7 @@ dependencies {
 | 
			
		||||
    implementation(libs.insetter)
 | 
			
		||||
    implementation(libs.markwon)
 | 
			
		||||
    implementation(libs.aboutLibraries.compose)
 | 
			
		||||
    implementation(libs.cascade)
 | 
			
		||||
 | 
			
		||||
    // Conductor
 | 
			
		||||
    implementation(libs.bundles.conductor)
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,8 @@ import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.SupervisorJob
 | 
			
		||||
import kotlinx.coroutines.cancel
 | 
			
		||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.catch
 | 
			
		||||
import kotlinx.coroutines.flow.launchIn
 | 
			
		||||
import kotlinx.coroutines.flow.onEach
 | 
			
		||||
@@ -47,6 +49,9 @@ class DownloadService : Service() {
 | 
			
		||||
         */
 | 
			
		||||
        val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
 | 
			
		||||
 | 
			
		||||
        private val _isRunning = MutableStateFlow(false)
 | 
			
		||||
        val isRunning = _isRunning.asStateFlow()
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Starts this service.
 | 
			
		||||
         *
 | 
			
		||||
@@ -98,6 +103,7 @@ class DownloadService : Service() {
 | 
			
		||||
        startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
 | 
			
		||||
        wakeLock = acquireWakeLock(javaClass.name)
 | 
			
		||||
        runningRelay.call(true)
 | 
			
		||||
        _isRunning.value = true
 | 
			
		||||
        subscriptions = CompositeSubscription()
 | 
			
		||||
        listenDownloaderState()
 | 
			
		||||
        listenNetworkChanges()
 | 
			
		||||
@@ -109,6 +115,7 @@ class DownloadService : Service() {
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        ioScope?.cancel()
 | 
			
		||||
        runningRelay.call(false)
 | 
			
		||||
        _isRunning.value = false
 | 
			
		||||
        subscriptions.unsubscribe()
 | 
			
		||||
        downloadManager.stopDownloads()
 | 
			
		||||
        wakeLock.releaseIfNeeded()
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,8 @@ class DownloadQueue(
 | 
			
		||||
        .startWith(Unit)
 | 
			
		||||
        .map { this }
 | 
			
		||||
 | 
			
		||||
    fun getUpdatedAsFlow(): Flow<List<Download>> = getUpdatedObservable().asFlow()
 | 
			
		||||
 | 
			
		||||
    private fun setPagesFor(download: Download) {
 | 
			
		||||
        if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
 | 
			
		||||
            setPagesSubject(download.pages, null)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,132 +1,316 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.download
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuInflater
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
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.Pause
 | 
			
		||||
import androidx.compose.material.icons.filled.PlayArrow
 | 
			
		||||
import androidx.compose.material.icons.outlined.MoreVert
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
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.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 androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
 | 
			
		||||
import dev.chrisbanes.insetter.applyInsetter
 | 
			
		||||
import eu.kanade.presentation.components.AppBar
 | 
			
		||||
import eu.kanade.presentation.components.EmptyScreen
 | 
			
		||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
 | 
			
		||||
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.DownloadControllerBinding
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.DownloadListBinding
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchUI
 | 
			
		||||
import me.saket.cascade.CascadeDropdownMenu
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the currently active downloads.
 | 
			
		||||
 * Uses R.layout.fragment_download_queue.
 | 
			
		||||
 */
 | 
			
		||||
class DownloadController :
 | 
			
		||||
    NucleusController<DownloadControllerBinding, DownloadPresenter>(),
 | 
			
		||||
    FabController,
 | 
			
		||||
    FullComposeController<DownloadPresenter>(),
 | 
			
		||||
    DownloadAdapter.DownloadItemListener {
 | 
			
		||||
 | 
			
		||||
    private lateinit var controllerBinding: DownloadListBinding
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private var adapter: DownloadAdapter? = null
 | 
			
		||||
    private var actionFab: ExtendedFloatingActionButton? = null
 | 
			
		||||
    private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Map of subscriptions for active downloads.
 | 
			
		||||
     */
 | 
			
		||||
    private val progressSubscriptions by lazy { mutableMapOf<Download, Subscription>() }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the download queue is running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private var isRunning: Boolean = false
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater)
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): DownloadPresenter {
 | 
			
		||||
        return DownloadPresenter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return resources?.getString(R.string.label_download_queue)
 | 
			
		||||
    }
 | 
			
		||||
    override fun createPresenter() = DownloadPresenter()
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        binding.recycler.applyInsetter {
 | 
			
		||||
            type(navigationBars = true) {
 | 
			
		||||
                padding()
 | 
			
		||||
            }
 | 
			
		||||
        viewScope.launchUI {
 | 
			
		||||
            presenter.getDownloadStatusFlow()
 | 
			
		||||
                .collect(this@DownloadController::onStatusChange)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter.
 | 
			
		||||
        adapter = DownloadAdapter(this@DownloadController)
 | 
			
		||||
        binding.recycler.adapter = adapter
 | 
			
		||||
        adapter?.isHandleDragEnabled = true
 | 
			
		||||
        adapter?.fastScroller = binding.fastScroller
 | 
			
		||||
 | 
			
		||||
        // Set the layout manager for the recycler and fixed size.
 | 
			
		||||
        binding.recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        binding.recycler.setHasFixedSize(true)
 | 
			
		||||
 | 
			
		||||
        actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
 | 
			
		||||
 | 
			
		||||
        // Subscribe to changes
 | 
			
		||||
        DownloadService.runningRelay
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .subscribeUntilDestroy { onQueueStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadStatusObservable()
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .subscribeUntilDestroy { onStatusChange(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.getDownloadProgressObservable()
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .subscribeUntilDestroy { onUpdateDownloadedPages(it) }
 | 
			
		||||
 | 
			
		||||
        presenter.downloadQueue.getUpdatedObservable()
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .subscribeUntilDestroy {
 | 
			
		||||
                updateTitle(it.size)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configureFab(fab: ExtendedFloatingActionButton) {
 | 
			
		||||
        actionFab = fab
 | 
			
		||||
        fab.setOnClickListener {
 | 
			
		||||
            val context = applicationContext ?: return@setOnClickListener
 | 
			
		||||
 | 
			
		||||
            if (isRunning) {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.pauseDownloads()
 | 
			
		||||
            } else {
 | 
			
		||||
                DownloadService.start(context)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setInformationView()
 | 
			
		||||
        viewScope.launchUI {
 | 
			
		||||
            presenter.getDownloadProgressFlow()
 | 
			
		||||
                .collect(this@DownloadController::onUpdateDownloadedPages)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupFab(fab: ExtendedFloatingActionButton) {
 | 
			
		||||
        fab.setOnClickListener(null)
 | 
			
		||||
        actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
 | 
			
		||||
        actionFab = null
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun ComposeContent() {
 | 
			
		||||
        val context = LocalContext.current
 | 
			
		||||
        val downloadList by presenter.state.collectAsState()
 | 
			
		||||
 | 
			
		||||
        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 (downloadList.isNotEmpty()) {
 | 
			
		||||
                                val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
 | 
			
		||||
                                Pill(
 | 
			
		||||
                                    text = "${downloadList.size}",
 | 
			
		||||
                                    modifier = Modifier.padding(start = 4.dp),
 | 
			
		||||
                                    color = MaterialTheme.colorScheme.onBackground
 | 
			
		||||
                                        .copy(alpha = pillAlpha),
 | 
			
		||||
                                    fontSize = 14.sp,
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    navigateUp = router::popCurrentController,
 | 
			
		||||
                    actions = {
 | 
			
		||||
                        if (downloadList.isNotEmpty()) {
 | 
			
		||||
                            val (expanded, onExpanded) = remember { mutableStateOf(false) }
 | 
			
		||||
                            Box {
 | 
			
		||||
                                IconButton(onClick = { onExpanded(!expanded) }) {
 | 
			
		||||
                                    Icon(
 | 
			
		||||
                                        imageVector = Icons.Outlined.MoreVert,
 | 
			
		||||
                                        contentDescription = stringResource(R.string.label_more),
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                                CascadeDropdownMenu(
 | 
			
		||||
                                    expanded = expanded,
 | 
			
		||||
                                    onDismissRequest = { onExpanded(false) },
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    DropdownMenuItem(
 | 
			
		||||
                                        text = { Text(text = stringResource(id = R.string.action_reorganize_by)) },
 | 
			
		||||
                                        children = {
 | 
			
		||||
                                            DropdownMenuItem(
 | 
			
		||||
                                                text = { Text(text = stringResource(id = R.string.action_order_by_upload_date)) },
 | 
			
		||||
                                                children = {
 | 
			
		||||
                                                    DropdownMenuItem(
 | 
			
		||||
                                                        text = { Text(text = stringResource(id = R.string.action_newest)) },
 | 
			
		||||
                                                        onClick = {
 | 
			
		||||
                                                            reorderQueue({ it.download.chapter.date_upload }, true)
 | 
			
		||||
                                                            onExpanded(false)
 | 
			
		||||
                                                        },
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                    DropdownMenuItem(
 | 
			
		||||
                                                        text = { Text(text = stringResource(id = R.string.action_oldest)) },
 | 
			
		||||
                                                        onClick = {
 | 
			
		||||
                                                            reorderQueue({ it.download.chapter.date_upload }, false)
 | 
			
		||||
                                                            onExpanded(false)
 | 
			
		||||
                                                        },
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                },
 | 
			
		||||
                                            )
 | 
			
		||||
                                            DropdownMenuItem(
 | 
			
		||||
                                                text = { Text(text = stringResource(id = R.string.action_order_by_chapter_number)) },
 | 
			
		||||
                                                children = {
 | 
			
		||||
                                                    DropdownMenuItem(
 | 
			
		||||
                                                        text = { Text(text = stringResource(id = R.string.action_asc)) },
 | 
			
		||||
                                                        onClick = {
 | 
			
		||||
                                                            reorderQueue({ it.download.chapter.chapter_number }, false)
 | 
			
		||||
                                                            onExpanded(false)
 | 
			
		||||
                                                        },
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                    DropdownMenuItem(
 | 
			
		||||
                                                        text = { Text(text = stringResource(id = R.string.action_desc)) },
 | 
			
		||||
                                                        onClick = {
 | 
			
		||||
                                                            reorderQueue({ it.download.chapter.chapter_number }, true)
 | 
			
		||||
                                                            onExpanded(false)
 | 
			
		||||
                                                        },
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                },
 | 
			
		||||
                                            )
 | 
			
		||||
                                        },
 | 
			
		||||
                                    )
 | 
			
		||||
                                    DropdownMenuItem(
 | 
			
		||||
                                        text = { Text(text = stringResource(id = R.string.action_cancel_all)) },
 | 
			
		||||
                                        onClick = {
 | 
			
		||||
                                            presenter.clearQueue(context)
 | 
			
		||||
                                            onExpanded(false)
 | 
			
		||||
                                        },
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    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.Default.Pause
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Icons.Default.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)
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
                        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) {
 | 
			
		||||
@@ -138,32 +322,6 @@ class DownloadController :
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
 | 
			
		||||
        inflater.inflate(R.menu.download_queue, menu)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
 | 
			
		||||
        menu.findItem(R.id.reorder).isVisible = !presenter.downloadQueue.isEmpty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        val context = applicationContext ?: return false
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.clear_queue -> {
 | 
			
		||||
                DownloadService.stop(context)
 | 
			
		||||
                presenter.clearQueue()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.newest, R.id.oldest -> {
 | 
			
		||||
                reorderQueue({ it.download.chapter.date_upload }, item.itemId == R.id.newest)
 | 
			
		||||
            }
 | 
			
		||||
            R.id.asc, R.id.desc -> {
 | 
			
		||||
                reorderQueue({ it.download.chapter.chapter_number }, item.itemId == R.id.desc)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
 | 
			
		||||
        val adapter = adapter ?: return
 | 
			
		||||
        val newDownloads = mutableListOf<Download>()
 | 
			
		||||
@@ -242,30 +400,6 @@ class DownloadController :
 | 
			
		||||
        progressSubscriptions.remove(download)?.unsubscribe()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the queue's status has changed. Updates the visibility of the buttons.
 | 
			
		||||
     *
 | 
			
		||||
     * @param running whether the queue is now running or not.
 | 
			
		||||
     */
 | 
			
		||||
    private fun onQueueStatusChange(running: Boolean) {
 | 
			
		||||
        isRunning = running
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
 | 
			
		||||
        // Check if download queue is empty and update information accordingly.
 | 
			
		||||
        setInformationView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called from the presenter to assign the downloads for the adapter.
 | 
			
		||||
     *
 | 
			
		||||
     * @param downloads the downloads from the queue.
 | 
			
		||||
     */
 | 
			
		||||
    fun onNextDownloads(downloads: List<DownloadHeaderItem>) {
 | 
			
		||||
        activity?.invalidateOptionsMenu()
 | 
			
		||||
        setInformationView()
 | 
			
		||||
        adapter?.updateDataSet(downloads)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the progress of a download changes.
 | 
			
		||||
     *
 | 
			
		||||
@@ -291,39 +425,7 @@ class DownloadController :
 | 
			
		||||
     * @return the holder of the download or null if it's not bound.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getHolder(download: Download): DownloadHolder? {
 | 
			
		||||
        return binding.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set information view when queue is empty
 | 
			
		||||
     */
 | 
			
		||||
    private fun setInformationView() {
 | 
			
		||||
        if (presenter.downloadQueue.isEmpty()) {
 | 
			
		||||
            binding.emptyView.show(R.string.information_no_downloads)
 | 
			
		||||
            actionFab?.isVisible = false
 | 
			
		||||
            updateTitle()
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.emptyView.hide()
 | 
			
		||||
            actionFab?.apply {
 | 
			
		||||
                isVisible = true
 | 
			
		||||
 | 
			
		||||
                setText(
 | 
			
		||||
                    if (isRunning) {
 | 
			
		||||
                        R.string.action_pause
 | 
			
		||||
                    } else {
 | 
			
		||||
                        R.string.action_resume
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                setIconResource(
 | 
			
		||||
                    if (isRunning) {
 | 
			
		||||
                        R.drawable.ic_pause_24dp
 | 
			
		||||
                    } else {
 | 
			
		||||
                        R.drawable.ic_play_arrow_24dp
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return controllerBinding.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -373,7 +475,7 @@ class DownloadController :
 | 
			
		||||
                        ?.filterIsInstance<DownloadItem>()
 | 
			
		||||
                        ?.map(DownloadItem::download)
 | 
			
		||||
                        ?.partition { item.download.manga.id == it.manga.id }
 | 
			
		||||
                        ?: Pair(listOf<Download>(), listOf<Download>())
 | 
			
		||||
                        ?: Pair(listOf(), listOf())
 | 
			
		||||
                    presenter.reorder(selectedSeries + otherSeries)
 | 
			
		||||
                }
 | 
			
		||||
                R.id.cancel_download -> {
 | 
			
		||||
@@ -391,14 +493,4 @@ class DownloadController :
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateTitle(queueSize: Int = 0) {
 | 
			
		||||
        val defaultTitle = getTitle()
 | 
			
		||||
 | 
			
		||||
        if (queueSize == 0) {
 | 
			
		||||
            setTitle(defaultTitle)
 | 
			
		||||
        } else {
 | 
			
		||||
            setTitle("$defaultTitle ($queueSize)")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,14 +35,25 @@ data class DownloadHeaderItem(
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other is DownloadHeaderItem) {
 | 
			
		||||
            return id == other.id && name == other.name
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
 | 
			
		||||
        other as DownloadHeaderItem
 | 
			
		||||
 | 
			
		||||
        if (id != other.id) return false
 | 
			
		||||
        if (name != other.name) return false
 | 
			
		||||
        if (size != other.size) return false
 | 
			
		||||
        if (subItemsCount != other.subItemsCount) return false
 | 
			
		||||
        if (subItems !== other.subItems) return false
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return id.hashCode()
 | 
			
		||||
        var result = id.hashCode()
 | 
			
		||||
        result = 31 * result + name.hashCode()
 | 
			
		||||
        result = 31 * result + size
 | 
			
		||||
        result = 31 * result + subItems.hashCode()
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,21 @@
 | 
			
		||||
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.data.download.model.DownloadQueue
 | 
			
		||||
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.collect
 | 
			
		||||
import kotlinx.coroutines.flow.map
 | 
			
		||||
import kotlinx.coroutines.flow.update
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -21,37 +28,34 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Property to get the queue from the download manager.
 | 
			
		||||
     */
 | 
			
		||||
    val downloadQueue: DownloadQueue
 | 
			
		||||
    private val downloadQueue: DownloadQueue
 | 
			
		||||
        get() = downloadManager.queue
 | 
			
		||||
 | 
			
		||||
    private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
 | 
			
		||||
    val state = _state.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        downloadQueue.getUpdatedObservable()
 | 
			
		||||
            .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
            .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) })
 | 
			
		||||
        presenterScope.launch {
 | 
			
		||||
            downloadQueue.getUpdatedAsFlow()
 | 
			
		||||
                .catch { error -> logcat(LogPriority.ERROR, error) }
 | 
			
		||||
                .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) })
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
            .subscribeLatestCache(DownloadController::onNextDownloads) { _, error ->
 | 
			
		||||
                logcat(LogPriority.ERROR, error)
 | 
			
		||||
            }
 | 
			
		||||
                }
 | 
			
		||||
                .collect { newList -> _state.update { newList } }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getDownloadStatusObservable(): Observable<Download> {
 | 
			
		||||
        return downloadQueue.getStatusObservable()
 | 
			
		||||
            .startWith(downloadQueue.getActiveDownloads())
 | 
			
		||||
    }
 | 
			
		||||
    fun getDownloadStatusFlow() = downloadQueue.getStatusAsFlow()
 | 
			
		||||
 | 
			
		||||
    fun getDownloadProgressObservable(): Observable<Download> {
 | 
			
		||||
        return downloadQueue.getProgressObservable()
 | 
			
		||||
            .onBackpressureBuffer()
 | 
			
		||||
    }
 | 
			
		||||
    fun getDownloadProgressFlow() = downloadQueue.getProgressAsFlow()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pauses the download queue.
 | 
			
		||||
@@ -63,7 +67,8 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Clears the download queue.
 | 
			
		||||
     */
 | 
			
		||||
    fun clearQueue() {
 | 
			
		||||
    fun clearQueue(context: Context) {
 | 
			
		||||
        DownloadService.stop(context)
 | 
			
		||||
        downloadManager.clearQueue()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_gravity="start"
 | 
			
		||||
            android:layout_marginEnd="4dp"
 | 
			
		||||
            android:paddingHorizontal="10dp"
 | 
			
		||||
            android:paddingVertical="8dp"
 | 
			
		||||
            android:scaleType="center"
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,7 @@
 | 
			
		||||
            android:id="@+id/menu"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginEnd="4dp"
 | 
			
		||||
            android:layout_toEndOf="@id/download_progress_text"
 | 
			
		||||
            android:background="?attr/selectableItemBackgroundBorderless"
 | 
			
		||||
            android:contentDescription="@string/action_menu"
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:clipToPadding="false"
 | 
			
		||||
        android:paddingBottom="@dimen/fab_list_padding"
 | 
			
		||||
        tools:listitem="@layout/download_item" />
 | 
			
		||||
 | 
			
		||||
    <eu.kanade.tachiyomi.widget.MaterialFastScroll
 | 
			
		||||
@@ -22,11 +21,4 @@
 | 
			
		||||
        app:fastScrollerBubbleEnabled="false"
 | 
			
		||||
        tools:visibility="visible" />
 | 
			
		||||
 | 
			
		||||
    <eu.kanade.tachiyomi.widget.EmptyView
 | 
			
		||||
        android:id="@+id/empty_view"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
</FrameLayout>
 | 
			
		||||
@@ -62,6 +62,7 @@ flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c801
 | 
			
		||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
 | 
			
		||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
 | 
			
		||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
 | 
			
		||||
cascade = "me.saket.cascade:cascade-compose:2.0.0-beta1"
 | 
			
		||||
 | 
			
		||||
conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" }
 | 
			
		||||
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user