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 ff8984b043..72f6975637 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 @@ -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()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index f7a8ea4741..e6cc4ddba6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -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) 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 e6d375d85c..1c569b0d87 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 @@ -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( +class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter>( null, controller, true @@ -28,12 +29,21 @@ class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter item.name + is DownloadItem -> item.download.manga.title + else -> "" + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt index 57721817eb..9674e5b40a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt @@ -19,7 +19,7 @@ class DownloadBottomPresenter(val sheet: DownloadBottomSheet) { * Download manager. */ val downloadManager: DownloadManager by injectLazy() - var items = listOf() + var items = listOf() 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt index 36511d9757..2e2f2870db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt @@ -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) { + fun onNextDownloads(downloads: List) { 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 > reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) { + val adapter = adapter ?: return + val newDownloads = mutableListOf() + 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() + 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() + ?.map(DownloadItem::download) + ?.partition { item.download.manga.id == it.manga.id } + ?: Pair(listOf(), listOf()) + presenter.reorder(selectedSeries + otherSeries) + } + R.id.cancel_series -> { + val allDownloadsForSeries = adapter?.currentItems + ?.filterIsInstance() + ?.filter { item.download.manga.id == it.download.manga.id } + ?.map(DownloadItem::download) + if (!allDownloadsForSeries.isNullOrEmpty()) { + presenter.cancelDownloads(allDownloadsForSeries) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt new file mode 100644 index 0000000000..59d604974b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt new file mode 100644 index 0000000000..18b9b3f090 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt @@ -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() { + + override fun getLayoutRes(): Int { + return R.layout.download_header + } + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter>, + ): DownloadHeaderHolder { + return DownloadHeaderHolder(view, adapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: DownloadHeaderHolder, + position: Int, + payloads: List?, + ) { + 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 + } +} 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 7c8806dbd7..fd8ffd69da 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 @@ -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) { 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 index 9c10f61e51..5106433291 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt @@ -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() { +class DownloadItem( + val download: Download, + header: DownloadHeaderItem, +) : AbstractSectionableItem(header) { /** * Whether this item is currently selected. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index c09ba8c5dd..30f1dc7c0f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index f1c4f9c160..c2e03d6b7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -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) } } } diff --git a/app/src/main/res/color/draggable_card_foreground.xml b/app/src/main/res/color/draggable_card_foreground.xml new file mode 100644 index 0000000000..ef3ba322b7 --- /dev/null +++ b/app/src/main/res/color/draggable_card_foreground.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/download_header.xml b/app/src/main/res/layout/download_header.xml new file mode 100644 index 0000000000..70ffda8bbd --- /dev/null +++ b/app/src/main/res/layout/download_header.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml index fc60349ad2..2fa72364ca 100644 --- a/app/src/main/res/layout/download_item.xml +++ b/app/src/main/res/layout/download_item.xml @@ -1,8 +1,7 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/menu/download_queue.xml b/app/src/main/res/menu/download_queue.xml index 33f4e79d03..5f0b358915 100644 --- a/app/src/main/res/menu/download_queue.xml +++ b/app/src/main/res/menu/download_queue.xml @@ -1,6 +1,6 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:id="@+id/action_sort_date" + android:title="@string/by_update_date" + app:showAsAction="never"> + + + + + + android:id="@+id/action_sort_chapter" + android:title="@string/by_chapter_number" + app:showAsAction="never"> + + + + + diff --git a/app/src/main/res/menu/download_single.xml b/app/src/main/res/menu/download_single.xml index b0db09552b..0da2189f4b 100644 --- a/app/src/main/res/menu/download_single.xml +++ b/app/src/main/res/menu/download_single.xml @@ -6,6 +6,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 25647f8ee1..94564737d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -873,6 +873,7 @@ Download queue Downloading: %1$s Cancel all + Move series to top Cancel all for this series Downloaded Not downloaded @@ -951,6 +952,7 @@ Alphabetically Always Always ask + Ascending Automatic Back BETA @@ -966,6 +968,7 @@ Cover of manga Create Date + Descending Default Delete Deleted: %1$s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 812b67d320..84030b2f26 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -87,6 +87,15 @@ 4dp + +