mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-21 20:47:03 -05:00
Add download queue features from J2K fork
This commit is contained in:
parent
3e5a48e5e4
commit
fb897e37d1
16 changed files with 439 additions and 127 deletions
|
@ -5,6 +5,7 @@ import com.hippo.unifile.UniFile
|
|||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
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.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
|
@ -19,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
|
|||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(context: Context) {
|
||||
class DownloadManager(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
|
@ -92,6 +93,29 @@ class DownloadManager(context: Context) {
|
|||
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.
|
||||
*
|
||||
|
@ -157,6 +181,15 @@ class DownloadManager(context: Context) {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -83,7 +83,8 @@ class Downloader(
|
|||
* Whether the downloader is running.
|
||||
*/
|
||||
@Volatile
|
||||
private var isRunning: Boolean = false
|
||||
var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
launchNow {
|
||||
|
|
|
@ -24,17 +24,30 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
|||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
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>?) {
|
||||
statusSubject = subject
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOT_DOWNLOADED = 0
|
||||
const val QUEUE = 1
|
||||
const val DOWNLOADING = 2
|
||||
|
|
|
@ -12,16 +12,18 @@ import rx.subjects.PublishSubject
|
|||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()
|
||||
) :
|
||||
List<Download> by queue {
|
||||
) : List<Download> by queue {
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
|
||||
private val updatedRelay = PublishRelay.create<Unit>()
|
||||
|
||||
private val downloadListeners = mutableListOf<DownloadListener>()
|
||||
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.setStatusCallback(::setPagesFor)
|
||||
download.status = Download.QUEUE
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
|
@ -33,6 +35,11 @@ class DownloadQueue(
|
|||
val removed = queue.remove(download)
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
}
|
||||
callListeners(download)
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
|
@ -55,6 +62,11 @@ class DownloadQueue(
|
|||
fun clear() {
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
}
|
||||
callListeners(download)
|
||||
}
|
||||
queue.clear()
|
||||
store.clear()
|
||||
|
@ -70,6 +82,24 @@ class DownloadQueue(
|
|||
.startWith(Unit)
|
||||
.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> {
|
||||
return statusSubject.onBackpressureBuffer()
|
||||
.startWith(getActiveDownloads())
|
||||
|
@ -77,12 +107,14 @@ class DownloadQueue(
|
|||
if (download.status == Download.DOWNLOADING) {
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
callListeners(download)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
callListeners(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,15 +20,23 @@ open class Page(
|
|||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var progress: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
@Transient
|
||||
private var statusCallback: ((Page) -> Unit)? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
|
@ -41,6 +49,10 @@ open class Page(
|
|||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Page) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
|
|
|
@ -1,71 +1,26 @@
|
|||
package eu.kanade.tachiyomi.ui.download
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
/**
|
||||
* Adapter storing a list of downloads.
|
||||
*
|
||||
* @param context the context of the fragment containing this adapter.
|
||||
*/
|
||||
class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
|
||||
|
||||
private var items = emptyList<Download>()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
class DownloadAdapter(controller: DownloadController) : FlexibleAdapter<DownloadItem>(
|
||||
null,
|
||||
controller,
|
||||
true
|
||||
) {
|
||||
|
||||
/**
|
||||
* Sets a list of downloads in the adapter.
|
||||
*
|
||||
* @param downloads the list to set.
|
||||
* Listener called when an item of the list is released.
|
||||
*/
|
||||
fun setItems(downloads: List<Download>) {
|
||||
items = downloads
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
val downloadItemListener: DownloadItemListener = controller
|
||||
|
||||
/**
|
||||
* Returns the number of downloads in the adapter
|
||||
*/
|
||||
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)
|
||||
interface DownloadItemListener {
|
||||
fun onItemReleased(position: Int)
|
||||
fun onMenuItemClick(position: Int, menuItem: MenuItem)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ import rx.android.schedulers.AndroidSchedulers
|
|||
* Controller that shows the currently active downloads.
|
||||
* Uses R.layout.fragment_download_queue.
|
||||
*/
|
||||
class DownloadController : NucleusController<DownloadPresenter>() {
|
||||
class DownloadController : NucleusController<DownloadPresenter>(),
|
||||
DownloadAdapter.DownloadItemListener {
|
||||
|
||||
/**
|
||||
* Adapter containing the active downloads.
|
||||
|
@ -64,14 +65,15 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
setInformationView()
|
||||
|
||||
// Initialize adapter.
|
||||
adapter = DownloadAdapter()
|
||||
adapter = DownloadAdapter(this@DownloadController)
|
||||
recycler.adapter = adapter
|
||||
adapter?.isHandleDragEnabled = true
|
||||
|
||||
// Set the layout manager for the recycler and fixed size.
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.setHasFixedSize(true)
|
||||
|
||||
// Suscribe to changes
|
||||
// Subscribe to changes
|
||||
DownloadService.runningRelay
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeUntilDestroy { onQueueStatusChange(it) }
|
||||
|
@ -99,14 +101,10 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
// Set start button visibility.
|
||||
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
|
||||
|
||||
// Set pause button visibility.
|
||||
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.reorder).isVisible = !presenter.downloadQueue.isEmpty()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -121,6 +119,16 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
DownloadService.stop(context)
|
||||
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)
|
||||
}
|
||||
|
@ -173,7 +181,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
// Avoid leaking subscriptions
|
||||
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.
|
||||
*/
|
||||
fun onNextDownloads(downloads: List<Download>) {
|
||||
fun onNextDownloads(downloads: List<DownloadItem>) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
setInformationView()
|
||||
adapter?.setItems(downloads)
|
||||
adapter?.updateDataSet(downloads)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -214,7 +222,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
*
|
||||
* @param download the download whose progress has changed.
|
||||
*/
|
||||
fun onUpdateProgress(download: Download) {
|
||||
private fun onUpdateProgress(download: Download) {
|
||||
getHolder(download)?.notifyProgress()
|
||||
}
|
||||
|
||||
|
@ -223,7 +231,7 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
*
|
||||
* @param download the download whose page has been downloaded.
|
||||
*/
|
||||
fun onUpdateDownloadedPages(download: Download) {
|
||||
private fun onUpdateDownloadedPages(download: Download) {
|
||||
getHolder(download)?.notifyDownloadedPages()
|
||||
}
|
||||
|
||||
|
@ -247,4 +255,48 @@ class DownloadController : NucleusController<DownloadPresenter>() {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package eu.kanade.tachiyomi.ui.download
|
||||
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
||||
import kotlinx.android.synthetic.main.download_item.view.chapter_title
|
||||
import kotlinx.android.synthetic.main.download_item.view.download_progress
|
||||
import kotlinx.android.synthetic.main.download_item.view.download_progress_text
|
||||
import kotlinx.android.synthetic.main.download_item.view.manga_title
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||
import kotlinx.android.synthetic.main.download_item.chapter_title
|
||||
import kotlinx.android.synthetic.main.download_item.download_progress
|
||||
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.
|
||||
|
@ -15,33 +19,37 @@ import kotlinx.android.synthetic.main.download_item.view.manga_title
|
|||
* @param view the inflated view for this 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
|
||||
|
||||
/**
|
||||
* Method called from [DownloadAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given download.
|
||||
* Binds this holder with the given category.
|
||||
*
|
||||
* @param download the download to bind.
|
||||
* @param category The category to bind.
|
||||
*/
|
||||
fun onSetValues(download: Download) {
|
||||
fun bind(download: Download) {
|
||||
this.download = download
|
||||
|
||||
// Update the chapter name.
|
||||
view.chapter_title.text = download.chapter.name
|
||||
chapter_title.text = download.chapter.name
|
||||
|
||||
// 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
|
||||
val pages = download.pages
|
||||
if (pages == null) {
|
||||
view.download_progress.progress = 0
|
||||
view.download_progress.max = 1
|
||||
view.download_progress_text.text = ""
|
||||
download_progress.progress = 0
|
||||
download_progress.max = 1
|
||||
download_progress_text.text = ""
|
||||
} else {
|
||||
view.download_progress.max = pages.size * 100
|
||||
download_progress.max = pages.size * 100
|
||||
notifyProgress()
|
||||
notifyDownloadedPages()
|
||||
}
|
||||
|
@ -52,10 +60,10 @@ class DownloadHolder(private val view: View) : BaseViewHolder(view) {
|
|||
*/
|
||||
fun notifyProgress() {
|
||||
val pages = download.pages ?: return
|
||||
if (view.download_progress.max == 1) {
|
||||
view.download_progress.max = pages.size * 100
|
||||
if (download_progress.max == 1) {
|
||||
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() {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.DownloadQueue
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import java.util.ArrayList
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
|
@ -16,9 +15,6 @@ import uy.kohesive.injekt.injectLazy
|
|||
*/
|
||||
class DownloadPresenter : BasePresenter<DownloadController>() {
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
|
@ -32,7 +28,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
|||
|
||||
downloadQueue.getUpdatedObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map { ArrayList(it) }
|
||||
.map { it.map(::DownloadItem) }
|
||||
.subscribeLatestCache(DownloadController::onNextDownloads) { _, error ->
|
||||
Timber.e(error)
|
||||
}
|
||||
|
@ -61,4 +57,12 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
|||
fun clearQueue() {
|
||||
downloadManager.clearQueue()
|
||||
}
|
||||
|
||||
fun reorder(downloads: List<Download>) {
|
||||
downloadManager.reorderQueue(downloads)
|
||||
}
|
||||
|
||||
fun cancelDownload(download: Download) {
|
||||
downloadManager.deletePendingDownload(download)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,17 @@ package eu.kanade.tachiyomi.util.view
|
|||
import android.graphics.Color
|
||||
import android.graphics.Point
|
||||
import android.graphics.Typeface
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
|
@ -36,6 +42,25 @@ inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Sn
|
|||
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() {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_more_vert_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_more_vert_24dp.xml
Normal 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>
|
|
@ -1,47 +1,89 @@
|
|||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/material_layout_keylines_screen_edge_margin"
|
||||
android:paddingTop="@dimen/material_component_lists_padding_above_list"
|
||||
android:paddingEnd="@dimen/material_layout_keylines_screen_edge_margin">
|
||||
android:paddingStart="0dp"
|
||||
android:paddingTop="@dimen/material_component_lists_padding_above_list">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/download_progress_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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"
|
||||
<ImageView
|
||||
android:id="@+id/reorder"
|
||||
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:layout_height="0dp"
|
||||
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:maxLines="1"
|
||||
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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapter_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/manga_title"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_toEndOf="@id/reorder"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
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" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/download_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
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>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
android:id="@+id/start_queue"
|
||||
android:icon="@drawable/ic_play_arrow_24dp"
|
||||
android:title="@string/action_start"
|
||||
android:visible="false"
|
||||
app:iconTint="?attr/colorOnPrimary"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
|
@ -14,14 +13,26 @@
|
|||
android:id="@+id/pause_queue"
|
||||
android:icon="@drawable/ic_pause_24dp"
|
||||
android:title="@string/action_pause"
|
||||
android:visible="false"
|
||||
app:iconTint="?attr/colorOnPrimary"
|
||||
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
|
||||
android:id="@+id/clear_queue"
|
||||
android:title="@string/action_cancel_all"
|
||||
android:visible="false"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
|
16
app/src/main/res/menu/download_single.xml
Normal file
16
app/src/main/res/menu/download_single.xml
Normal 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>
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
<!-- Actions -->
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_menu">Menu</string>
|
||||
<string name="action_filter">Filter</string>
|
||||
<string name="action_filter_downloaded">Downloaded</string>
|
||||
<string name="action_filter_bookmarked">Bookmarked</string>
|
||||
|
@ -87,6 +88,11 @@
|
|||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_cancel_all">Cancel all</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_share">Share</string>
|
||||
<string name="action_save">Save</string>
|
||||
|
|
Loading…
Reference in a new issue