Add download queue features from J2K fork

This commit is contained in:
arkon 2020-02-23 12:42:10 -05:00
parent 3e5a48e5e4
commit fb897e37d1
16 changed files with 439 additions and 127 deletions

View File

@ -5,6 +5,7 @@ import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
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.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
@ -19,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
*/ */
class DownloadManager(context: Context) { class DownloadManager(private val context: Context) {
/** /**
* The sources manager. * The sources manager.
@ -92,6 +93,29 @@ class DownloadManager(context: Context) {
downloader.clearQueue(isNotification) downloader.clearQueue(isNotification)
} }
/**
* Reorders the download queue.
*
* @param downloads value to set the download queue to
*/
fun reorderQueue(downloads: List<Download>) {
val wasRunning = downloader.isRunning
if (downloads.isEmpty()) {
DownloadService.stop(context)
downloader.queue.clear()
return
}
downloader.pause()
downloader.queue.clear()
downloader.queue.addAll(downloads)
if (wasRunning) {
downloader.start()
}
}
/** /**
* Tells the downloader to enqueue the given list of chapters. * Tells the downloader to enqueue the given list of chapters.
* *
@ -157,6 +181,15 @@ class DownloadManager(context: Context) {
return cache.getDownloadCount(manga) return cache.getDownloadCount(manga)
} }
/**
* Calls delete chapter, which deletes a temp download.
*
* @param download the download to cancel.
*/
fun deletePendingDownload(download: Download) {
deleteChapters(listOf(download.chapter), download.manga, download.source)
}
/** /**
* Deletes the directories of a list of downloaded chapters. * Deletes the directories of a list of downloaded chapters.
* *

View File

@ -83,7 +83,8 @@ class Downloader(
* Whether the downloader is running. * Whether the downloader is running.
*/ */
@Volatile @Volatile
private var isRunning: Boolean = false var isRunning: Boolean = false
private set
init { init {
launchNow { launchNow {

View File

@ -24,17 +24,30 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
set(status) { set(status) {
field = status field = status
statusSubject?.onNext(this) statusSubject?.onNext(this)
statusCallback?.invoke(this)
} }
@Transient @Transient
private var statusSubject: PublishSubject<Download>? = null private var statusSubject: PublishSubject<Download>? = null
@Transient
private var statusCallback: ((Download) -> Unit)? = null
val progress: Int
get() {
val pages = pages ?: return 0
return pages.map(Page::progress).average().toInt()
}
fun setStatusSubject(subject: PublishSubject<Download>?) { fun setStatusSubject(subject: PublishSubject<Download>?) {
statusSubject = subject statusSubject = subject
} }
companion object { fun setStatusCallback(f: ((Download) -> Unit)?) {
statusCallback = f
}
companion object {
const val NOT_DOWNLOADED = 0 const val NOT_DOWNLOADED = 0
const val QUEUE = 1 const val QUEUE = 1
const val DOWNLOADING = 2 const val DOWNLOADING = 2

View File

@ -12,16 +12,18 @@ import rx.subjects.PublishSubject
class DownloadQueue( class DownloadQueue(
private val store: DownloadStore, private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>() private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()
) : ) : List<Download> by queue {
List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>() private val statusSubject = PublishSubject.create<Download>()
private val updatedRelay = PublishRelay.create<Unit>() private val updatedRelay = PublishRelay.create<Unit>()
private val downloadListeners = mutableListOf<DownloadListener>()
fun addAll(downloads: List<Download>) { fun addAll(downloads: List<Download>) {
downloads.forEach { download -> downloads.forEach { download ->
download.setStatusSubject(statusSubject) download.setStatusSubject(statusSubject)
download.setStatusCallback(::setPagesFor)
download.status = Download.QUEUE download.status = Download.QUEUE
} }
queue.addAll(downloads) queue.addAll(downloads)
@ -33,6 +35,11 @@ class DownloadQueue(
val removed = queue.remove(download) val removed = queue.remove(download)
store.remove(download) store.remove(download)
download.setStatusSubject(null) download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
callListeners(download)
if (removed) { if (removed) {
updatedRelay.call(Unit) updatedRelay.call(Unit)
} }
@ -55,6 +62,11 @@ class DownloadQueue(
fun clear() { fun clear() {
queue.forEach { download -> queue.forEach { download ->
download.setStatusSubject(null) download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
callListeners(download)
} }
queue.clear() queue.clear()
store.clear() store.clear()
@ -70,6 +82,24 @@ class DownloadQueue(
.startWith(Unit) .startWith(Unit)
.map { this } .map { this }
private fun setPagesFor(download: Download) {
if (download.status == Download.DOWNLOADING) {
download.pages?.forEach { page ->
page.setStatusCallback {
callListeners(download)
}
}
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
callListeners(download)
}
private fun callListeners(download: Download) {
downloadListeners.forEach { it.updateDownload(download) }
}
fun getProgressObservable(): Observable<Download> { fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer() return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads()) .startWith(getActiveDownloads())
@ -77,12 +107,14 @@ class DownloadQueue(
if (download.status == Download.DOWNLOADING) { if (download.status == Download.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>() val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject) setPagesSubject(download.pages, pageStatusSubject)
callListeners(download)
return@flatMap pageStatusSubject return@flatMap pageStatusSubject
.onBackpressureBuffer() .onBackpressureBuffer()
.filter { it == Page.READY } .filter { it == Page.READY }
.map { download } .map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null) setPagesSubject(download.pages, null)
callListeners(download)
} }
Observable.just(download) Observable.just(download)
} }
@ -96,4 +128,16 @@ class DownloadQueue(
} }
} }
} }
fun addListener(listener: DownloadListener) {
downloadListeners.add(listener)
}
fun removeListener(listener: DownloadListener) {
downloadListeners.remove(listener)
}
interface DownloadListener {
fun updateDownload(download: Download)
}
} }

View File

@ -20,15 +20,23 @@ open class Page(
set(value) { set(value) {
field = value field = value
statusSubject?.onNext(value) statusSubject?.onNext(value)
statusCallback?.invoke(this)
} }
@Transient @Transient
@Volatile @Volatile
var progress: Int = 0 var progress: Int = 0
set(value) {
field = value
statusCallback?.invoke(this)
}
@Transient @Transient
private var statusSubject: Subject<Int, Int>? = null private var statusSubject: Subject<Int, Int>? = null
@Transient
private var statusCallback: ((Page) -> Unit)? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = if (contentLength > 0) { progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt() (100 * bytesRead / contentLength).toInt()
@ -41,6 +49,10 @@ open class Page(
this.statusSubject = subject this.statusSubject = subject
} }
fun setStatusCallback(f: ((Page) -> Unit)?) {
statusCallback = f
}
companion object { companion object {
const val QUEUE = 0 const val QUEUE = 0
const val LOAD_PAGE = 1 const val LOAD_PAGE = 1

View File

@ -1,71 +1,26 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.view.ViewGroup import android.view.MenuItem
import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.util.view.inflate
/** /**
* Adapter storing a list of downloads. * Adapter storing a list of downloads.
* *
* @param context the context of the fragment containing this adapter. * @param context the context of the fragment containing this adapter.
*/ */
class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() { class DownloadAdapter(controller: DownloadController) : FlexibleAdapter<DownloadItem>(
null,
private var items = emptyList<Download>() controller,
true
init { ) {
setHasStableIds(true)
}
/** /**
* Sets a list of downloads in the adapter. * Listener called when an item of the list is released.
*
* @param downloads the list to set.
*/ */
fun setItems(downloads: List<Download>) { val downloadItemListener: DownloadItemListener = controller
items = downloads
notifyDataSetChanged()
}
/** interface DownloadItemListener {
* Returns the number of downloads in the adapter fun onItemReleased(position: Int)
*/ fun onMenuItemClick(position: Int, menuItem: MenuItem)
override fun getItemCount(): Int {
return items.size
}
/**
* Returns the identifier for a download.
*
* @param position the position in the adapter.
* @return an identifier for the item.
*/
override fun getItemId(position: Int): Long {
return items[position].chapter.id!!
}
/**
* Creates a new view holder.
*
* @param parent the parent view.
* @param viewType the type of the holder.
* @return a new view holder for a manga.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
val view = parent.inflate(R.layout.download_item)
return DownloadHolder(view)
}
/**
* Binds a holder with a new position.
*
* @param holder the holder to bind.
* @param position the position to bind.
*/
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val download = items[position]
holder.onSetValues(download)
} }
} }

View File

@ -24,7 +24,8 @@ import rx.android.schedulers.AndroidSchedulers
* 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 : NucleusController<DownloadPresenter>() { class DownloadController : NucleusController<DownloadPresenter>(),
DownloadAdapter.DownloadItemListener {
/** /**
* Adapter containing the active downloads. * Adapter containing the active downloads.
@ -64,14 +65,15 @@ class DownloadController : NucleusController<DownloadPresenter>() {
setInformationView() setInformationView()
// Initialize adapter. // Initialize adapter.
adapter = DownloadAdapter() adapter = DownloadAdapter(this@DownloadController)
recycler.adapter = adapter recycler.adapter = adapter
adapter?.isHandleDragEnabled = true
// Set the layout manager for the recycler and fixed size. // Set the layout manager for the recycler and fixed size.
recycler.layoutManager = LinearLayoutManager(view.context) recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
// Suscribe to changes // Subscribe to changes
DownloadService.runningRelay DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeUntilDestroy { onQueueStatusChange(it) } .subscribeUntilDestroy { onQueueStatusChange(it) }
@ -99,14 +101,10 @@ class DownloadController : NucleusController<DownloadPresenter>() {
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
// Set start button visibility.
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
// Set pause button visibility.
menu.findItem(R.id.pause_queue).isVisible = isRunning menu.findItem(R.id.pause_queue).isVisible = isRunning
// Set clear button visibility.
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -121,6 +119,16 @@ class DownloadController : NucleusController<DownloadPresenter>() {
DownloadService.stop(context) DownloadService.stop(context)
presenter.clearQueue() presenter.clearQueue()
} }
R.id.newest, R.id.oldest -> {
val adapter = adapter ?: return false
val items = adapter.currentItems.sortedBy { it.download.chapter.date_upload }
.toMutableList()
if (item.itemId == R.id.newest)
items.reverse()
adapter.updateDataSet(items)
val downloads = items.mapNotNull { it.download }
presenter.reorder(downloads)
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@ -173,7 +181,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
// Avoid leaking subscriptions // Avoid leaking subscriptions
progressSubscriptions.remove(download)?.unsubscribe() progressSubscriptions.remove(download)?.unsubscribe()
progressSubscriptions.put(download, subscription) progressSubscriptions[download] = subscription
} }
/** /**
@ -203,10 +211,10 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* *
* @param downloads the downloads from the queue. * @param downloads the downloads from the queue.
*/ */
fun onNextDownloads(downloads: List<Download>) { fun onNextDownloads(downloads: List<DownloadItem>) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
setInformationView() setInformationView()
adapter?.setItems(downloads) adapter?.updateDataSet(downloads)
} }
/** /**
@ -214,7 +222,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* *
* @param download the download whose progress has changed. * @param download the download whose progress has changed.
*/ */
fun onUpdateProgress(download: Download) { private fun onUpdateProgress(download: Download) {
getHolder(download)?.notifyProgress() getHolder(download)?.notifyProgress()
} }
@ -223,7 +231,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
* *
* @param download the download whose page has been downloaded. * @param download the download whose page has been downloaded.
*/ */
fun onUpdateDownloadedPages(download: Download) { private fun onUpdateDownloadedPages(download: Download) {
getHolder(download)?.notifyDownloadedPages() getHolder(download)?.notifyDownloadedPages()
} }
@ -247,4 +255,48 @@ class DownloadController : NucleusController<DownloadPresenter>() {
empty_view?.hide() empty_view?.hide()
} }
} }
/**
* 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 = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.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) {
when (menuItem.itemId) {
R.id.move_to_top, R.id.move_to_bottom -> {
val items = adapter?.currentItems?.toMutableList() ?: return
val item = items[position]
items.remove(item)
if (menuItem.itemId == R.id.move_to_top)
items.add(0, item)
else
items.add(item)
adapter?.updateDataSet(items)
val downloads = items.mapNotNull { it.download }
presenter.reorder(downloads)
}
R.id.cancel_download -> {
val download = adapter?.getItem(position)?.download ?: return
presenter.cancelDownload(download)
adapter?.removeItem(position)
val adapter = adapter ?: return
val downloads =
(0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download }
presenter.reorder(downloads)
}
}
}
} }

View File

@ -1,12 +1,16 @@
package eu.kanade.tachiyomi.ui.download package eu.kanade.tachiyomi.ui.download
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.download_item.view.chapter_title import eu.kanade.tachiyomi.util.view.popupMenu
import kotlinx.android.synthetic.main.download_item.view.download_progress import kotlinx.android.synthetic.main.download_item.chapter_title
import kotlinx.android.synthetic.main.download_item.view.download_progress_text import kotlinx.android.synthetic.main.download_item.download_progress
import kotlinx.android.synthetic.main.download_item.view.manga_title import kotlinx.android.synthetic.main.download_item.download_progress_text
import kotlinx.android.synthetic.main.download_item.manga_full_title
import kotlinx.android.synthetic.main.download_item.menu
import kotlinx.android.synthetic.main.download_item.reorder
/** /**
* Class used to hold the data of a download. * Class used to hold the data of a download.
@ -15,33 +19,37 @@ import kotlinx.android.synthetic.main.download_item.view.manga_title
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @constructor creates a new download holder. * @constructor creates a new download holder.
*/ */
class DownloadHolder(private val view: View) : BaseViewHolder(view) { class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
BaseFlexibleViewHolder(view, adapter) {
init {
setDragHandleView(reorder)
menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
private lateinit var download: Download private lateinit var download: Download
/** /**
* Method called from [DownloadAdapter.onBindViewHolder]. It updates the data for this * Binds this holder with the given category.
* holder with the given download.
* *
* @param download the download to bind. * @param category The category to bind.
*/ */
fun onSetValues(download: Download) { fun bind(download: Download) {
this.download = download this.download = download
// Update the chapter name. // Update the chapter name.
view.chapter_title.text = download.chapter.name chapter_title.text = download.chapter.name
// Update the manga title // Update the manga title
view.manga_title.text = download.manga.title manga_full_title.text = download.manga.title
// Update the progress bar and the number of downloaded pages // Update the progress bar and the number of downloaded pages
val pages = download.pages val pages = download.pages
if (pages == null) { if (pages == null) {
view.download_progress.progress = 0 download_progress.progress = 0
view.download_progress.max = 1 download_progress.max = 1
view.download_progress_text.text = "" download_progress_text.text = ""
} else { } else {
view.download_progress.max = pages.size * 100 download_progress.max = pages.size * 100
notifyProgress() notifyProgress()
notifyDownloadedPages() notifyDownloadedPages()
} }
@ -52,10 +60,10 @@ class DownloadHolder(private val view: View) : BaseViewHolder(view) {
*/ */
fun notifyProgress() { fun notifyProgress() {
val pages = download.pages ?: return val pages = download.pages ?: return
if (view.download_progress.max == 1) { if (download_progress.max == 1) {
view.download_progress.max = pages.size * 100 download_progress.max = pages.size * 100
} }
view.download_progress.progress = download.totalProgress download_progress.progress = download.totalProgress
} }
/** /**
@ -63,6 +71,22 @@ class DownloadHolder(private val view: View) : BaseViewHolder(view) {
*/ */
fun notifyDownloadedPages() { fun notifyDownloadedPages() {
val pages = download.pages ?: return val pages = download.pages ?: return
view.download_progress_text.text = "${download.downloadedImages}/${pages.size}" download_progress_text.text = "${download.downloadedImages}/${pages.size}"
}
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
adapter.downloadItemListener.onItemReleased(position)
}
private fun showPopupMenu(view: View) {
view.popupMenu(R.menu.download_single, {
findItem(R.id.move_to_top).isVisible = adapterPosition != 0
findItem(R.id.move_to_bottom).isVisible =
adapterPosition != adapter.itemCount - 1
}, {
adapter.downloadItemListener.onMenuItemClick(adapterPosition, this)
true
})
} }
} }

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
class DownloadItem(val download: Download) : AbstractFlexibleItem<DownloadHolder>() {
override fun getLayoutRes(): Int {
return R.layout.download_item
}
/**
* Returns a new view holder for this item.
*
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
): DownloadHolder {
return DownloadHolder(view, adapter as DownloadAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: DownloadHolder,
position: Int,
payloads: MutableList<Any>
) {
holder.bind(download)
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is DownloadItem) {
return download.chapter.id == other.download.chapter.id
}
return false
}
override fun hashCode(): Int {
return download.chapter.id!!.toInt()
}
}

View File

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
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 java.util.ArrayList
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
@ -16,9 +15,6 @@ import uy.kohesive.injekt.injectLazy
*/ */
class DownloadPresenter : BasePresenter<DownloadController>() { class DownloadPresenter : BasePresenter<DownloadController>() {
/**
* Download manager.
*/
val downloadManager: DownloadManager by injectLazy() val downloadManager: DownloadManager by injectLazy()
/** /**
@ -32,7 +28,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
downloadQueue.getUpdatedObservable() downloadQueue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.map { ArrayList(it) } .map { it.map(::DownloadItem) }
.subscribeLatestCache(DownloadController::onNextDownloads) { _, error -> .subscribeLatestCache(DownloadController::onNextDownloads) { _, error ->
Timber.e(error) Timber.e(error)
} }
@ -61,4 +57,12 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
fun clearQueue() { fun clearQueue() {
downloadManager.clearQueue() downloadManager.clearQueue()
} }
fun reorder(downloads: List<Download>) {
downloadManager.reorderQueue(downloads)
}
fun cancelDownload(download: Download) {
downloadManager.deletePendingDownload(download)
}
} }

View File

@ -5,11 +5,17 @@ package eu.kanade.tachiyomi.util.view
import android.graphics.Color import android.graphics.Color
import android.graphics.Point import android.graphics.Point
import android.graphics.Typeface import android.graphics.Typeface
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import kotlin.math.min import kotlin.math.min
/** /**
@ -36,6 +42,25 @@ inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Sn
return snack return snack
} }
/**
* Shows a popup menu on top of this view.
*
* @param menuRes menu items to inflate the menu with.
* @param initMenu function to execute when the menu after is inflated.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
fun View.popupMenu(@MenuRes menuRes: Int, initMenu: (Menu.() -> Unit)? = null, onMenuItemClick: MenuItem.() -> Boolean) {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
popup.menuInflater.inflate(menuRes, popup.menu)
if (initMenu != null) {
popup.menu.initMenu()
}
popup.setOnMenuItemClickListener { it.onMenuItemClick() }
popup.show()
}
inline fun View.visible() { inline fun View.visible() {
visibility = View.VISIBLE visibility = View.VISIBLE
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -1,47 +1,89 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/material_layout_keylines_screen_edge_margin" android:paddingStart="0dp"
android:paddingTop="@dimen/material_component_lists_padding_above_list" android:paddingTop="@dimen/material_component_lists_padding_above_list">
android:paddingEnd="@dimen/material_layout_keylines_screen_edge_margin">
<TextView <ImageView
android:id="@+id/download_progress_text" android:id="@+id/reorder"
android:layout_width="wrap_content" android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_alignParentEnd="true"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint"
tools:text="(0/10)" />
<TextView
android:id="@+id/manga_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_toStartOf="@id/download_progress_text" android:layout_gravity="start"
android:scaleType="center"
android:tint="?android:attr/textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_reorder_grey_24dp" />
<TextView
android:id="@+id/manga_full_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toEndOf="@id/reorder"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Body1" android:textAppearance="@style/TextAppearance.Regular.Body1"
app:layout_constraintEnd_toStartOf="@+id/download_progress_text"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toTopOf="parent"
tools:text="Manga title" /> tools:text="Manga title" />
<TextView <TextView
android:id="@+id/chapter_title" android:id="@+id/chapter_title"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/manga_title" android:layout_marginTop="4dp"
android:layout_toEndOf="@id/reorder"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption" android:textAppearance="@style/TextAppearance.Regular.Caption"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintStart_toStartOf="@+id/manga_full_title"
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
tools:text="Chapter Title" /> tools:text="Chapter Title" />
<ProgressBar <ProgressBar
android:id="@+id/download_progress" android:id="@+id/download_progress"
style="?android:attr/progressBarStyleHorizontal" style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/chapter_title" /> android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toBottomOf="@+id/chapter_title" />
</RelativeLayout> <TextView
android:id="@+id/download_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/manga_full_title"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint"
app:layout_constraintBottom_toBottomOf="@+id/manga_full_title"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintTop_toTopOf="@+id/manga_full_title"
tools:text="(0/10)" />
<ImageButton
android:id="@+id/menu"
android:layout_width="44dp"
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
android:layout_toEndOf="@id/download_progress_text"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/action_menu"
android:paddingStart="10dp"
android:paddingEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_more_vert_24dp"
app:tint="?attr/colorOnBackground" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,7 +6,6 @@
android:id="@+id/start_queue" android:id="@+id/start_queue"
android:icon="@drawable/ic_play_arrow_24dp" android:icon="@drawable/ic_play_arrow_24dp"
android:title="@string/action_start" android:title="@string/action_start"
android:visible="false"
app:iconTint="?attr/colorOnPrimary" app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
@ -14,14 +13,26 @@
android:id="@+id/pause_queue" android:id="@+id/pause_queue"
android:icon="@drawable/ic_pause_24dp" android:icon="@drawable/ic_pause_24dp"
android:title="@string/action_pause" android:title="@string/action_pause"
android:visible="false"
app:iconTint="?attr/colorOnPrimary" app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/reorder"
android:title="@string/action_reorganize_by"
app:showAsAction="never">
<menu>
<item
android:id="@+id/newest"
android:title="@string/action_newest" />
<item
android:id="@+id/oldest"
android:title="@string/action_oldest" />
</menu>
</item>
<item <item
android:id="@+id/clear_queue" android:id="@+id/clear_queue"
android:title="@string/action_cancel_all" android:title="@string/action_cancel_all"
android:visible="false"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/move_to_top"
android:title="@string/action_move_to_top" />
<item
android:id="@+id/move_to_bottom"
android:title="@string/action_move_to_bottom" />
<item
android:id="@+id/cancel_download"
android:title="@string/action_cancel" />
</menu>

View File

@ -29,6 +29,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_menu">Menu</string>
<string name="action_filter">Filter</string> <string name="action_filter">Filter</string>
<string name="action_filter_downloaded">Downloaded</string> <string name="action_filter_downloaded">Downloaded</string>
<string name="action_filter_bookmarked">Bookmarked</string> <string name="action_filter_bookmarked">Bookmarked</string>
@ -87,6 +88,11 @@
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_cancel_all">Cancel all</string> <string name="action_cancel_all">Cancel all</string>
<string name="action_sort">Sort</string> <string name="action_sort">Sort</string>
<string name="action_reorganize_by">Reorder</string>
<string name="action_newest">Newest</string>
<string name="action_oldest">Oldest</string>
<string name="action_move_to_top">Move to top</string>
<string name="action_move_to_bottom">Move to bottom</string>
<string name="action_install">Install</string> <string name="action_install">Install</string>
<string name="action_share">Share</string> <string name="action_share">Share</string>
<string name="action_save">Save</string> <string name="action_save">Save</string>