From fb897e37d1a0f68355bccbbfd22a91813906b5b5 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 23 Feb 2020 12:42:10 -0500 Subject: [PATCH] Add download queue features from J2K fork --- .../data/download/DownloadManager.kt | 35 +++++++- .../tachiyomi/data/download/Downloader.kt | 3 +- .../tachiyomi/data/download/model/Download.kt | 15 +++- .../data/download/model/DownloadQueue.kt | 48 +++++++++- .../eu/kanade/tachiyomi/source/model/Page.kt | 12 +++ .../tachiyomi/ui/download/DownloadAdapter.kt | 69 +++------------ .../ui/download/DownloadController.kt | 78 +++++++++++++--- .../tachiyomi/ui/download/DownloadHolder.kt | 66 +++++++++----- .../tachiyomi/ui/download/DownloadItem.kt | 65 ++++++++++++++ .../ui/download/DownloadPresenter.kt | 14 +-- .../tachiyomi/util/view/ViewExtensions.kt | 25 ++++++ .../main/res/drawable/ic_more_vert_24dp.xml | 9 ++ app/src/main/res/layout/download_item.xml | 88 ++++++++++++++----- app/src/main/res/menu/download_queue.xml | 17 +++- app/src/main/res/menu/download_single.xml | 16 ++++ app/src/main/res/values/strings.xml | 6 ++ 16 files changed, 439 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt create mode 100644 app/src/main/res/drawable/ic_more_vert_24dp.xml create mode 100644 app/src/main/res/menu/download_single.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index a938724bf8..d01d227fed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -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) { + 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. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index cc82b848c2..d144073600 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 5ef87892bf..5bd9b15bed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -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? = 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?) { 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index b912b033a0..4d9773d22e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -12,16 +12,18 @@ import rx.subjects.PublishSubject class DownloadQueue( private val store: DownloadStore, private val queue: MutableList = CopyOnWriteArrayList() -) : - List by queue { +) : List by queue { private val statusSubject = PublishSubject.create() private val updatedRelay = PublishRelay.create() + private val downloadListeners = mutableListOf() + fun addAll(downloads: List) { 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 { return statusSubject.onBackpressureBuffer() .startWith(getActiveDownloads()) @@ -77,12 +107,14 @@ class DownloadQueue( if (download.status == Download.DOWNLOADING) { val pageStatusSubject = PublishSubject.create() 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) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt index 1bd2c39e50..22436bfe8c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -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? = 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt index 994b83e8f2..01c72f2df6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt @@ -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() { - - private var items = emptyList() - - init { - setHasStableIds(true) - } +class DownloadAdapter(controller: DownloadController) : FlexibleAdapter( + 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) { - 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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt index dcb32127c2..ddf548c33e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt @@ -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() { +class DownloadController : NucleusController(), + DownloadAdapter.DownloadItemListener { /** * Adapter containing the active downloads. @@ -64,14 +65,15 @@ class DownloadController : NucleusController() { 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() { } 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() { 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() { // Avoid leaking subscriptions progressSubscriptions.remove(download)?.unsubscribe() - progressSubscriptions.put(download, subscription) + progressSubscriptions[download] = subscription } /** @@ -203,10 +211,10 @@ class DownloadController : NucleusController() { * * @param downloads the downloads from the queue. */ - fun onNextDownloads(downloads: List) { + fun onNextDownloads(downloads: List) { activity?.invalidateOptionsMenu() setInformationView() - adapter?.setItems(downloads) + adapter?.updateDataSet(downloads) } /** @@ -214,7 +222,7 @@ class DownloadController : NucleusController() { * * @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() { * * @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() { 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) + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index f82a606a2b..0f1ea754e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -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 + }) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt new file mode 100644 index 0000000000..e523bbe02f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt @@ -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() { + + 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> + ): 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>, + holder: DownloadHolder, + position: Int, + payloads: MutableList + ) { + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index 54135f6b63..b4c7b0c2fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -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() { - /** - * Download manager. - */ val downloadManager: DownloadManager by injectLazy() /** @@ -32,7 +28,7 @@ class DownloadPresenter : BasePresenter() { 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() { fun clearQueue() { downloadManager.clearQueue() } + + fun reorder(downloads: List) { + downloadManager.reorderQueue(downloads) + } + + fun cancelDownload(download: Download) { + downloadManager.deletePendingDownload(download) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index e92ec9f5d0..40676f574d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -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 } diff --git a/app/src/main/res/drawable/ic_more_vert_24dp.xml b/app/src/main/res/drawable/ic_more_vert_24dp.xml new file mode 100644 index 0000000000..568cbb4d26 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml index 67c6f47e57..24507324d2 100644 --- a/app/src/main/res/layout/download_item.xml +++ b/app/src/main/res/layout/download_item.xml @@ -1,47 +1,89 @@ - + android:paddingStart="0dp" + android:paddingTop="@dimen/material_component_lists_padding_above_list"> - - - + + + 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" /> - + + + + + diff --git a/app/src/main/res/menu/download_queue.xml b/app/src/main/res/menu/download_queue.xml index 35b1621111..63812170bc 100644 --- a/app/src/main/res/menu/download_queue.xml +++ b/app/src/main/res/menu/download_queue.xml @@ -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" /> + + + + + + + diff --git a/app/src/main/res/menu/download_single.xml b/app/src/main/res/menu/download_single.xml new file mode 100644 index 0000000000..b3f3e6b2fd --- /dev/null +++ b/app/src/main/res/menu/download_single.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8a5156913..c703862703 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Settings + Menu Filter Downloaded Bookmarked @@ -87,6 +88,11 @@ Cancel Cancel all Sort + Reorder + Newest + Oldest + Move to top + Move to bottom Install Share Save