Add download queue features from J2K fork

pull/2722/head
arkon 5 years ago
parent 3e5a48e5e4
commit fb897e37d1

@ -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)
}
/**
* Sets a list of downloads in the adapter.
*
* @param downloads the list to set.
*/
fun setItems(downloads: List<Download>) {
items = downloads
notifyDataSetChanged()
}
/**
* Returns the number of downloads in the adapter
*/
override fun getItemCount(): Int {
return items.size
}
class DownloadAdapter(controller: DownloadController) : FlexibleAdapter<DownloadItem>(
null,
controller,
true
) {
/**
* Returns the identifier for a download.
*
* @param position the position in the adapter.
* @return an identifier for the item.
* Listener called when an item of the list is released.
*/
override fun getItemId(position: Int): Long {
return items[position].chapter.id!!
}
val downloadItemListener: DownloadItemListener = controller
/**
* 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
}

@ -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)" />
<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_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_title"
android:layout_width="match_parent"
android:id="@+id/manga_full_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/download_progress_text"
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" />
<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" />
</RelativeLayout>
</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>

@ -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…
Cancel
Save