Updates to Downloads UI

Add background to draggable items - tachiyomiorg/tachiyomi@73e5e9ecd9
Grouped chapter download list by source - tachiyomiorg/tachiyomi@9106fc5b94
Add "Move all chapters from series to top" option to download context menu - tachiyomiorg/tachiyomi@3aa4e6eb93
add sort by chapter number in download queue - tachiyomiorg/tachiyomi@a083e1f71a

Fixes to fab pause/resume
Closes #1205

Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
Co-Authored-By: Franco Olivera <franco.olivera@fing.edu.uy>
Co-Authored-By: Riztard Lanthorn <16263232+Riztard@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2022-04-21 15:52:41 -04:00
parent 7363603732
commit 6174bced03
18 changed files with 309 additions and 65 deletions

View file

@ -69,7 +69,9 @@ class DownloadManager(val context: Context) {
* @return true if it's started, false otherwise (empty queue).
*/
fun startDownloads(): Boolean {
return downloader.start()
val hasStarted = downloader.start()
DownloadService.callListeners(hasStarted)
return hasStarted
}
/**
@ -112,6 +114,7 @@ class DownloadManager(val context: Context) {
if (isPaused()) {
if (DownloadService.isRunning(context)) {
downloader.start()
DownloadService.callListeners(true)
} else {
DownloadService.start(context)
}
@ -135,6 +138,7 @@ class DownloadManager(val context: Context) {
downloader.queue.addAll(downloads)
if (!wasPaused) {
downloader.start()
DownloadService.callListeners(true)
}
}
@ -243,6 +247,7 @@ class DownloadManager(val context: Context) {
downloader.queue.remove(chapters)
if (!wasPaused && downloader.queue.isNotEmpty()) {
downloader.start()
DownloadService.callListeners(true)
} else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) {
DownloadService.stop(context)
} else if (downloader.queue.isEmpty()) {

View file

@ -55,7 +55,7 @@ class DownloadService : Service() {
fun callListeners(downloading: Boolean? = null) {
val downloadManager: DownloadManager by injectLazy()
listeners.forEach {
it.downloadStatusChanged(downloading ?: downloadManager.hasQueue())
it.downloadStatusChanged(downloading ?: !downloadManager.isPaused())
}
}
@ -158,7 +158,7 @@ class DownloadService : Service() {
subscriptions.unsubscribe()
connectivityManager.unregisterNetworkCallback(networkCallback)
downloadManager.stopDownloads()
callListeners(downloadManager.hasQueue())
callListeners(false)
wakeLock.releaseIfNeeded()
if (LibraryUpdateService.runExtensionUpdatesAfter) {
ExtensionUpdateJob.runJobAgain(this, NetworkType.CONNECTED)

View file

@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
/**
* Adapter storing a list of downloads.
*
* @param context the context of the fragment containing this adapter.
*/
class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter<DownloadItem>(
class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter<AbstractFlexibleItem<*>>(
null,
controller,
true
@ -28,12 +29,21 @@ class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter<Downlo
fun onMenuItemClick(position: Int, menuItem: MenuItem)
}
override fun shouldMove(fromPosition: Int, toPosition: Int): Boolean {
// Don't let sub-items changing group
return getHeaderOf(getItem(fromPosition)) == getHeaderOf(getItem(toPosition))
}
override fun onItemSwiped(position: Int, direction: Int) {
super.onItemSwiped(position, direction)
downloadItemListener.onItemRemoved(position)
}
override fun onCreateBubbleText(position: Int): String {
return getItem(position)?.download?.manga?.title ?: ""
return when (val item = getItem(position)) {
is DownloadHeaderItem -> item.name
is DownloadItem -> item.download.manga.title
else -> ""
}
}
}

View file

@ -19,7 +19,7 @@ class DownloadBottomPresenter(val sheet: DownloadBottomSheet) {
* Download manager.
*/
val downloadManager: DownloadManager by injectLazy()
var items = listOf<DownloadItem>()
var items = listOf<DownloadHeaderItem>()
private var scope = CoroutineScope(Job() + Dispatchers.Default)
@ -31,13 +31,27 @@ class DownloadBottomPresenter(val sheet: DownloadBottomSheet) {
fun getItems() {
scope.launch {
val items = downloadQueue.map(::DownloadItem)
val hasChanged = if (this@DownloadBottomPresenter.items.size != items.size) true
val items = downloadQueue
.groupBy { it.source }
.map { entry ->
DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
addSubItems(0, entry.value.map { DownloadItem(it, this) })
}
}
val hasChanged = if (this@DownloadBottomPresenter.items.size != items.size ||
this@DownloadBottomPresenter.items.sumOf { it.subItemsCount } != items.sumOf { it.subItemsCount }
) true
else {
val oldItemsIds = this@DownloadBottomPresenter.items.mapNotNull {
it.download.chapter.id
}.toLongArray()
val newItemsIds = items.mapNotNull { it.download.chapter.id }.toLongArray()
val oldItemsIds = this@DownloadBottomPresenter.items.map { header ->
header.subItems.mapNotNull { it.download.chapter.id }
}
.flatten()
.toLongArray()
val newItemsIds = items.map { header ->
header.subItems.mapNotNull { it.download.chapter.id }
}
.flatten()
.toLongArray()
!oldItemsIds.contentEquals(newItemsIds)
}
this@DownloadBottomPresenter.items = items

View file

@ -93,7 +93,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
}
updateFab()
}
update()
update(!presenter.downloadManager.isPaused())
setInformationView()
if (!controller.hasQueue()) {
sheetBehavior?.isHideable = true
@ -101,9 +101,9 @@ class DownloadBottomSheet @JvmOverloads constructor(
}
}
fun update() {
fun update(isRunning: Boolean) {
presenter.getItems()
onQueueStatusChange(!presenter.downloadManager.isPaused())
onQueueStatusChange(isRunning)
binding.downloadFab.isInvisible = presenter.downloadQueue.isEmpty()
prepareMenu()
}
@ -140,7 +140,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
*
* @param downloads the downloads from the queue.
*/
fun onNextDownloads(downloads: List<DownloadItem>) {
fun onNextDownloads(downloads: List<DownloadHeaderItem>) {
prepareMenu()
setInformationView()
adapter?.updateDataSet(downloads)
@ -214,20 +214,30 @@ class DownloadBottomSheet @JvmOverloads constructor(
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)
reorderQueue({ it.download.chapter.date_upload }, item.itemId == R.id.newest)
}
R.id.asc, R.id.desc -> {
reorderQueue({ it.download.chapter.chapter_number }, item.itemId == R.id.desc)
}
}
return true
}
private fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) {
val adapter = adapter ?: return
val newDownloads = mutableListOf<Download>()
adapter.headerItems.forEach { headerItem ->
headerItem as DownloadHeaderItem
headerItem.subItems = headerItem.subItems.sortedBy(selector).toMutableList().apply {
if (reverse) {
reverse()
}
}
newDownloads.addAll(headerItem.subItems.map { it.download })
}
presenter.reorder(newDownloads)
}
fun dismiss() {
if (sheetBehavior?.isHideable == true) {
sheetBehavior?.hide()
@ -256,17 +266,25 @@ class DownloadBottomSheet @JvmOverloads constructor(
*/
override fun onItemReleased(position: Int) {
val adapter = adapter ?: return
val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download }
val downloads = adapter.headerItems.flatMap { header ->
adapter.getSectionItems(header).map { item ->
(item as DownloadItem).download
}
}
presenter.reorder(downloads)
}
override fun onItemRemoved(position: Int) {
val download = adapter?.getItem(position)?.download ?: return
val download = (adapter?.getItem(position) as? DownloadItem)?.download ?: return
presenter.cancelDownload(download)
adapter?.removeItem(position)
val adapter = adapter ?: return
val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download }
val downloads = adapter.headerItems.flatMap { header ->
adapter.getSectionItems(header).map { item ->
(item as DownloadItem).download
}
}
presenter.reorder(downloads)
controller.updateChapterDownload(download, false)
}
@ -278,27 +296,42 @@ class DownloadBottomSheet @JvmOverloads constructor(
* @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)
val item = adapter?.getItem(position) ?: return
if (item is DownloadItem) {
when (menuItem.itemId) {
R.id.move_to_top, R.id.move_to_bottom -> {
val headerItems = adapter?.headerItems ?: return
val newDownloads = mutableListOf<Download>()
headerItems.forEach { headerItem ->
headerItem as DownloadHeaderItem
if (headerItem == item.header) {
headerItem.removeSubItem(item)
if (menuItem.itemId == R.id.move_to_top) {
headerItem.addSubItem(0, item)
} else {
headerItem.addSubItem(item)
}
}
newDownloads.addAll(headerItem.subItems.map { it.download })
}
presenter.reorder(newDownloads)
}
adapter?.updateDataSet(items)
val downloads = items.mapNotNull { it.download }
presenter.reorder(downloads)
}
R.id.cancel_series -> {
val download = adapter?.getItem(position)?.download ?: return
val allDownloadsForSeries = adapter?.currentItems
?.filter { download.manga.id == it.download.manga.id }
?.map(DownloadItem::download)
if (!allDownloadsForSeries.isNullOrEmpty()) {
presenter.cancelDownloads(allDownloadsForSeries)
R.id.move_to_top_series -> {
val (selectedSeries, otherSeries) = adapter?.currentItems
?.filterIsInstance<DownloadItem>()
?.map(DownloadItem::download)
?.partition { item.download.manga.id == it.manga.id }
?: Pair(listOf<Download>(), listOf<Download>())
presenter.reorder(selectedSeries + otherSeries)
}
R.id.cancel_series -> {
val allDownloadsForSeries = adapter?.currentItems
?.filterIsInstance<DownloadItem>()
?.filter { item.download.manga.id == it.download.manga.id }
?.map(DownloadItem::download)
if (!allDownloadsForSeries.isNullOrEmpty()) {
presenter.cancelDownloads(allDownloadsForSeries)
}
}
}
}

View file

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.ui.download
import android.annotation.SuppressLint
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.ExpandableViewHolder
import eu.kanade.tachiyomi.databinding.DownloadHeaderBinding
class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter) {
private val binding = DownloadHeaderBinding.bind(view)
@SuppressLint("SetTextI18n")
fun bind(item: DownloadHeaderItem) {
setDragHandleView(binding.reorder)
binding.title.text = "${item.name} (${item.size})"
}
override fun onActionStateChanged(position: Int, actionState: Int) {
super.onActionStateChanged(position, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
binding.container.isDragged = true
mAdapter.collapseAll()
}
}
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
binding.container.isDragged = false
mAdapter as DownloadAdapter
mAdapter.expandAll()
mAdapter.downloadItemListener.onItemReleased(position)
}
}

View file

@ -0,0 +1,54 @@
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.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
data class DownloadHeaderItem(
val id: Long,
val name: String,
val size: Int,
) : AbstractExpandableHeaderItem<DownloadHeaderHolder, DownloadItem>() {
override fun getLayoutRes(): Int {
return R.layout.download_header
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
): DownloadHeaderHolder {
return DownloadHeaderHolder(view, adapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: DownloadHeaderHolder,
position: Int,
payloads: List<Any?>?,
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is DownloadHeaderItem) {
return id == other.id && name == other.name
}
return false
}
override fun hashCode(): Int {
return id.hashCode()
}
init {
isHidden = false
isExpanded = true
isSelectable = false
isSwipeable = false
}
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.download
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.isVisible
import androidx.recyclerview.widget.ItemTouchHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.DownloadItemBinding
@ -77,9 +78,18 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
binding.downloadProgressText.text = "${download.downloadedImages}/${pages.size}"
}
override fun onActionStateChanged(position: Int, actionState: Int) {
super.onActionStateChanged(position, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
binding.root.isDragged = true
}
}
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
adapter.downloadItemListener.onItemReleased(position)
binding.root.isDragged = false
binding.root.cardElevation = 0f
}
private fun showPopupMenu(view: View) {

View file

@ -3,12 +3,15 @@ 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.AbstractSectionableItem
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>() {
class DownloadItem(
val download: Download,
header: DownloadHeaderItem,
) : AbstractSectionableItem<DownloadHolder, DownloadHeaderItem>(header) {
/**
* Whether this item is currently selected.

View file

@ -474,7 +474,7 @@ class RecentsController(bundle: Bundle? = null) :
refresh()
}
setBottomPadding()
binding.downloadBottomSheet.dlBottomSheet.update()
binding.downloadBottomSheet.dlBottomSheet.update(!presenter.downloadManager.isPaused())
if (BuildConfig.DEBUG && query.isBlank() && isControllerVisible) {
val searchItem =
@ -560,7 +560,7 @@ class RecentsController(bundle: Bundle? = null) :
fun updateChapterDownload(download: Download, updateDLSheet: Boolean = true) {
if (view == null) return
if (updateDLSheet) {
binding.downloadBottomSheet.dlBottomSheet.update()
binding.downloadBottomSheet.dlBottomSheet.update(!presenter.downloadManager.isPaused())
binding.downloadBottomSheet.dlBottomSheet.onUpdateProgress(download)
binding.downloadBottomSheet.dlBottomSheet.onUpdateDownloadedPages(download)
}
@ -569,8 +569,8 @@ class RecentsController(bundle: Bundle? = null) :
holder.notifyStatus(download.status, download.progress, download.chapter.read, true)
}
fun updateDownloadStatus() {
binding.downloadBottomSheet.dlBottomSheet.update()
fun updateDownloadStatus(isRunning: Boolean) {
binding.downloadBottomSheet.dlBottomSheet.update(isRunning)
}
private fun refreshItem(chapterId: Long) {

View file

@ -378,7 +378,7 @@ class RecentsPresenter(
setDownloadedChapters(recentItems)
withContext(Dispatchers.Main) {
controller?.showLists(recentItems, true)
controller?.updateDownloadStatus()
controller?.updateDownloadStatus(!downloadManager.isPaused())
}
}
}
@ -386,7 +386,7 @@ class RecentsPresenter(
override fun downloadStatusChanged(downloading: Boolean) {
presenterScope.launch {
withContext(Dispatchers.Main) {
controller?.updateDownloadStatus()
controller?.updateDownloadStatus(downloading)
}
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:alpha="0.08" android:color="?attr/colorSecondary" app:state_dragged="true" />
<item android:alpha="0.08" android:color="?attr/colorSecondary" android:state_activated="true" />
<item android:color="@android:color/transparent" />
</selector>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:id="@+id/container"
style="@style/Widget.Tachiyomi.CardView.Draggable"
android:layout_marginTop="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
android:textAppearance="?textAppearanceLabelLarge"
android:textColor="?android:textColorSecondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
tools:text="Title" />
<ImageView
android:id="@+id/reorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingHorizontal="10dp"
android:paddingVertical="8dp"
android:scaleType="center"
app:srcCompat="@drawable/ic_drag_handle_24dp"
app:tint="?android:attr/textColorHint"
tools:ignore="ContentDescription" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Tachiyomi.CardView.Draggable"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
@ -129,4 +128,4 @@
app:srcCompat="@drawable/ic_more_vert_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/reorder"
@ -9,11 +9,31 @@
app:showAsAction="never">
<menu>
<item
android:id="@+id/newest"
android:title="@string/newest"/>
android:id="@+id/action_sort_date"
android:title="@string/by_update_date"
app:showAsAction="never">
<menu>
<item
android:id="@+id/newest"
android:title="@string/newest" />
<item
android:id="@+id/oldest"
android:title="@string/oldest" />
</menu>
</item>
<item
android:id="@+id/oldest"
android:title="@string/oldest"/>
android:id="@+id/action_sort_chapter"
android:title="@string/by_chapter_number"
app:showAsAction="never">
<menu>
<item
android:id="@+id/asc"
android:title="@string/ascending" />
<item
android:id="@+id/desc"
android:title="@string/descending" />
</menu>
</item>
</menu>
</item>

View file

@ -6,6 +6,9 @@
<item android:id="@+id/move_to_bottom"
android:title="@string/move_to_bottom" />
<item android:id="@+id/move_to_top_series"
android:title="@string/move_series_to_top" />
<item android:id="@+id/cancel_series"
android:title="@string/cancel_all_for_series" />
</menu>

View file

@ -873,6 +873,7 @@
<string name="download_queue">Download queue</string>
<string name="downloading_">Downloading: %1$s</string>
<string name="cancel_all">Cancel all</string>
<string name="move_series_to_top">Move series to top</string>
<string name="cancel_all_for_series">Cancel all for this series</string>
<string name="downloaded">Downloaded</string>
<string name="not_downloaded">Not downloaded</string>
@ -951,6 +952,7 @@
<string name="alphabetically">Alphabetically</string>
<string name="always">Always</string>
<string name="always_ask">Always ask</string>
<string name="ascending">Ascending</string>
<string name="automatic">Automatic</string>
<string name="back">Back</string>
<string name="beta">BETA</string>
@ -966,6 +968,7 @@
<string name="cover_of_image">Cover of manga</string>
<string name="create">Create</string>
<string name="date">Date</string>
<string name="descending">Descending</string>
<string name="default_value">Default</string>
<string name="delete">Delete</string>
<string name="deleted_">Deleted: %1$s</string>

View file

@ -87,6 +87,15 @@
<item name="cardElevation">4dp</item>
</style>
<style name="Widget.Tachiyomi.CardView.Draggable" parent="Widget.Material3.CardView.Elevated">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="cardCornerRadius">0dp</item>
<item name="cardBackgroundColor">?background</item>
<item name="cardForegroundColor">@color/draggable_card_foreground</item>
<item name="cardElevation">0dp</item>
</style>
<style name="Theme.Widget.GridView">
<item name="android:smoothScrollbar">true</item>
<item name="android:numColumns">auto_fit</item>