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:
parent
07d1b9f3ba
commit
fb9791f597
10 changed files with 335 additions and 221 deletions
|
@ -73,6 +73,7 @@ android {
|
||||||
signingConfig = debugType.signingConfig
|
signingConfig = debugType.signingConfig
|
||||||
versionNameSuffix = debugType.versionNameSuffix
|
versionNameSuffix = debugType.versionNameSuffix
|
||||||
applicationIdSuffix = debugType.applicationIdSuffix
|
applicationIdSuffix = debugType.applicationIdSuffix
|
||||||
|
matchingFallbacks.add("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +253,7 @@ dependencies {
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.markwon)
|
implementation(libs.markwon)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
|
implementation(libs.cascade)
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation(libs.bundles.conductor)
|
implementation(libs.bundles.conductor)
|
||||||
|
|
|
@ -25,6 +25,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
@ -47,6 +49,9 @@ class DownloadService : Service() {
|
||||||
*/
|
*/
|
||||||
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
val runningRelay: BehaviorRelay<Boolean> = BehaviorRelay.create(false)
|
||||||
|
|
||||||
|
private val _isRunning = MutableStateFlow(false)
|
||||||
|
val isRunning = _isRunning.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts this service.
|
* Starts this service.
|
||||||
*
|
*
|
||||||
|
@ -98,6 +103,7 @@ class DownloadService : Service() {
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
|
_isRunning.value = true
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
listenDownloaderState()
|
listenDownloaderState()
|
||||||
listenNetworkChanges()
|
listenNetworkChanges()
|
||||||
|
@ -109,6 +115,7 @@ class DownloadService : Service() {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
ioScope?.cancel()
|
ioScope?.cancel()
|
||||||
runningRelay.call(false)
|
runningRelay.call(false)
|
||||||
|
_isRunning.value = false
|
||||||
subscriptions.unsubscribe()
|
subscriptions.unsubscribe()
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads()
|
||||||
wakeLock.releaseIfNeeded()
|
wakeLock.releaseIfNeeded()
|
||||||
|
|
|
@ -83,6 +83,8 @@ class DownloadQueue(
|
||||||
.startWith(Unit)
|
.startWith(Unit)
|
||||||
.map { this }
|
.map { this }
|
||||||
|
|
||||||
|
fun getUpdatedAsFlow(): Flow<List<Download>> = getUpdatedObservable().asFlow()
|
||||||
|
|
||||||
private fun setPagesFor(download: Download) {
|
private fun setPagesFor(download: Download) {
|
||||||
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||||
setPagesSubject(download.pages, null)
|
setPagesSubject(download.pages, null)
|
||||||
|
|
|
@ -1,132 +1,316 @@
|
||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
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.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import eu.kanade.presentation.components.AppBar
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
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.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
import me.saket.cascade.CascadeDropdownMenu
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller that shows the currently active downloads.
|
* Controller that shows the currently active downloads.
|
||||||
* Uses R.layout.fragment_download_queue.
|
* Uses R.layout.fragment_download_queue.
|
||||||
*/
|
*/
|
||||||
class DownloadController :
|
class DownloadController :
|
||||||
NucleusController<DownloadControllerBinding, DownloadPresenter>(),
|
FullComposeController<DownloadPresenter>(),
|
||||||
FabController,
|
|
||||||
DownloadAdapter.DownloadItemListener {
|
DownloadAdapter.DownloadItemListener {
|
||||||
|
|
||||||
|
private lateinit var controllerBinding: DownloadListBinding
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter containing the active downloads.
|
* Adapter containing the active downloads.
|
||||||
*/
|
*/
|
||||||
private var adapter: DownloadAdapter? = null
|
private var adapter: DownloadAdapter? = null
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
|
||||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of subscriptions for active downloads.
|
* Map of subscriptions for active downloads.
|
||||||
*/
|
*/
|
||||||
private val progressSubscriptions by lazy { mutableMapOf<Download, Subscription>() }
|
private val progressSubscriptions by lazy { mutableMapOf<Download, Subscription>() }
|
||||||
|
|
||||||
/**
|
override fun createPresenter() = DownloadPresenter()
|
||||||
* 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 onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
binding.recycler.applyInsetter {
|
viewScope.launchUI {
|
||||||
type(navigationBars = true) {
|
presenter.getDownloadStatusFlow()
|
||||||
padding()
|
.collect(this@DownloadController::onStatusChange)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
viewScope.launchUI {
|
||||||
// Check if download queue is empty and update information accordingly.
|
presenter.getDownloadProgressFlow()
|
||||||
setInformationView()
|
.collect(this@DownloadController::onUpdateDownloadedPages)
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
@Composable
|
||||||
fab.setOnClickListener(null)
|
override fun ComposeContent() {
|
||||||
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
|
val context = LocalContext.current
|
||||||
actionFab = null
|
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) {
|
override fun onDestroyView(view: View) {
|
||||||
|
@ -138,32 +322,6 @@ class DownloadController :
|
||||||
super.onDestroyView(view)
|
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) {
|
private fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
val newDownloads = mutableListOf<Download>()
|
val newDownloads = mutableListOf<Download>()
|
||||||
|
@ -242,30 +400,6 @@ class DownloadController :
|
||||||
progressSubscriptions.remove(download)?.unsubscribe()
|
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.
|
* 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.
|
* @return the holder of the download or null if it's not bound.
|
||||||
*/
|
*/
|
||||||
private fun getHolder(download: Download): DownloadHolder? {
|
private fun getHolder(download: Download): DownloadHolder? {
|
||||||
return binding.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder
|
return controllerBinding.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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -373,7 +475,7 @@ class DownloadController :
|
||||||
?.filterIsInstance<DownloadItem>()
|
?.filterIsInstance<DownloadItem>()
|
||||||
?.map(DownloadItem::download)
|
?.map(DownloadItem::download)
|
||||||
?.partition { item.download.manga.id == it.manga.id }
|
?.partition { item.download.manga.id == it.manga.id }
|
||||||
?: Pair(listOf<Download>(), listOf<Download>())
|
?: Pair(listOf(), listOf())
|
||||||
presenter.reorder(selectedSeries + otherSeries)
|
presenter.reorder(selectedSeries + otherSeries)
|
||||||
}
|
}
|
||||||
R.id.cancel_download -> {
|
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 {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other is DownloadHeaderItem) {
|
if (javaClass != other?.javaClass) return false
|
||||||
return id == other.id && name == other.name
|
|
||||||
}
|
other as DownloadHeaderItem
|
||||||
return false
|
|
||||||
|
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 {
|
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 {
|
init {
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
package eu.kanade.tachiyomi.ui.download
|
package eu.kanade.tachiyomi.ui.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
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.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
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 logcat.LogPriority
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,37 +28,34 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||||
/**
|
/**
|
||||||
* Property to get the queue from the download manager.
|
* Property to get the queue from the download manager.
|
||||||
*/
|
*/
|
||||||
val downloadQueue: DownloadQueue
|
private val downloadQueue: DownloadQueue
|
||||||
get() = downloadManager.queue
|
get() = downloadManager.queue
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(emptyList<DownloadHeaderItem>())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
downloadQueue.getUpdatedObservable()
|
presenterScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
downloadQueue.getUpdatedAsFlow()
|
||||||
.map { downloads ->
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
downloads
|
.map { downloads ->
|
||||||
.groupBy { it.source }
|
downloads
|
||||||
.map { entry ->
|
.groupBy { it.source }
|
||||||
DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
|
.map { entry ->
|
||||||
addSubItems(0, entry.value.map { DownloadItem(it, this) })
|
DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
|
||||||
|
addSubItems(0, entry.value.map { DownloadItem(it, this) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.collect { newList -> _state.update { newList } }
|
||||||
.subscribeLatestCache(DownloadController::onNextDownloads) { _, error ->
|
}
|
||||||
logcat(LogPriority.ERROR, error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadStatusObservable(): Observable<Download> {
|
fun getDownloadStatusFlow() = downloadQueue.getStatusAsFlow()
|
||||||
return downloadQueue.getStatusObservable()
|
|
||||||
.startWith(downloadQueue.getActiveDownloads())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDownloadProgressObservable(): Observable<Download> {
|
fun getDownloadProgressFlow() = downloadQueue.getProgressAsFlow()
|
||||||
return downloadQueue.getProgressObservable()
|
|
||||||
.onBackpressureBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the download queue.
|
* Pauses the download queue.
|
||||||
|
@ -63,7 +67,8 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||||
/**
|
/**
|
||||||
* Clears the download queue.
|
* Clears the download queue.
|
||||||
*/
|
*/
|
||||||
fun clearQueue() {
|
fun clearQueue(context: Context) {
|
||||||
|
DownloadService.stop(context)
|
||||||
downloadManager.clearQueue()
|
downloadManager.clearQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
android:paddingHorizontal="10dp"
|
android:paddingHorizontal="10dp"
|
||||||
android:paddingVertical="8dp"
|
android:paddingVertical="8dp"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
android:id="@+id/menu"
|
android:id="@+id/menu"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
android:layout_toEndOf="@id/download_progress_text"
|
android:layout_toEndOf="@id/download_progress_text"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/action_menu"
|
android:contentDescription="@string/action_menu"
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="@dimen/fab_list_padding"
|
|
||||||
tools:listitem="@layout/download_item" />
|
tools:listitem="@layout/download_item" />
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||||
|
@ -22,11 +21,4 @@
|
||||||
app:fastScrollerBubbleEnabled="false"
|
app:fastScrollerBubbleEnabled="false"
|
||||||
tools:visibility="visible" />
|
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>
|
</FrameLayout>
|
|
@ -62,6 +62,7 @@ flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c801
|
||||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
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-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" }
|
||||||
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }
|
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }
|
||||||
|
|
Reference in a new issue