The Downloads Update

Pending downloads can be move to the top or bottom of the queue with a menu button on the right side, they can also be cancelled
Mass Migration now shows progress and total manga in the title
Fixed the issue where deleted downloads using the old folder format said they weren't (they were)
Fixed cancelled downloads not deleting the temp folder
Changed the format of downloads yet again, now it's just chapter id and the name
Added option to reorder pending downloads by newest or oldest chapter
This commit is contained in:
Jay 2020-01-07 23:32:30 -08:00
parent b6e4869d30
commit 68c3d28b4b
16 changed files with 268 additions and 75 deletions

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -203,13 +204,15 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
@Synchronized
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
fun removeChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
for (chapter in chapters) {
val chapterDirName = provider.getChapterDirName(chapter)
if (chapterDirName in mangaDir.files) {
mangaDir.files -= chapterDirName
val list = provider.getValidChapterDirNames(chapter)
list.forEach {
if (it in mangaDir.files) {
mangaDir.files -= it
}
}
}
}

View File

@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadManager(context: Context) {
class DownloadManager(val context: Context) {
/**
* The sources manager.
@ -99,7 +99,12 @@ class DownloadManager(context: Context) {
* @param downloads value to set the download queue to
*/
fun reorderQueue(downloads: List<Download>) {
val wasPaused = downloader.isPaused()
val wasPaused = isPaused()
if (downloads.isEmpty()) {
DownloadService.stop(context)
downloader.queue.clear()
return
}
downloader.pause()
downloader.queue.clear()
downloader.queue.addAll(downloads)
@ -108,6 +113,8 @@ class DownloadManager(context: Context) {
}
}
fun isPaused() = downloader.isPaused()
/**
* Tells the downloader to enqueue the given list of chapters.
@ -175,7 +182,7 @@ class DownloadManager(context: Context) {
}
/**
* Deletes the directories of a list of downloaded chapters.
* Deletes the directories of a list of partially downloaded chapters.
*
* @param chapters the list of chapters to delete.
* @param manga the manga of the chapters.
@ -183,9 +190,9 @@ class DownloadManager(context: Context) {
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
queue.remove(chapters)
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(chapters, manga)
cache.removeChapters(chapters, manga, source)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
}

View File

@ -95,6 +95,19 @@ class DownloadProvider(private val context: Context) {
return chapters.flatMap { getValidChapterDirNames(it) }.mapNotNull { mangaDir.findFile(it) }
}
/**
* Returns a list of downloaded directories for the chapters that exist.
*
* @param chapters the chapters to query.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findTempChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") }
}
/**
* Returns the download directory name for a source.
*
@ -119,6 +132,16 @@ class DownloadProvider(private val context: Context) {
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter): String {
return DiskUtil.buildValidFilename("${chapter.id} - ${chapter.name}")
}
/**
* Returns the chapter directory name for a chapter (that used the scanlator
*
* @param chapter the chapter to query.
*/
//TODO: Delete this in due time. N2Self, merging that pr was a mistake
private fun getChapterDirNameWithScanlator(chapter: Chapter): String {
return DiskUtil.buildValidFilename("${chapter.id}_${chapter.scanlator}_${chapter.name}")
}
@ -129,10 +152,10 @@ class DownloadProvider(private val context: Context) {
*/
fun getValidChapterDirNames(chapter: Chapter): List<String> {
return listOf(
getChapterDirName(chapter),
// Legacy chapter directory name used in v0.8.4 and before
DiskUtil.buildValidFilename(chapter.name)
getChapterDirName(chapter),
// Legacy chapter directory name used in v0.8.4 and before
getChapterDirNameWithScanlator(chapter),
DiskUtil.buildValidFilename(chapter.name)
)
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
@ -13,12 +14,13 @@ class DownloadAdapter(controller: DownloadController) : FlexibleAdapter<Download
/**
* Listener called when an item of the list is released.
*/
val onItemReleaseListener: OnItemReleaseListener = controller
val downloadItemListener: DownloadItemListener = controller
interface OnItemReleaseListener {
interface DownloadItemListener {
/**
* Called when an item of the list is released.
*/
fun onItemReleased(position: Int)
fun onMenuItemClick(position: Int, menuItem: MenuItem)
}
}

View File

@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
@ -17,6 +18,8 @@ import kotlinx.android.synthetic.main.download_controller.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy
import java.util.HashMap
import java.util.concurrent.TimeUnit
@ -25,7 +28,7 @@ import java.util.concurrent.TimeUnit
* Uses R.layout.fragment_download_queue.
*/
class DownloadController : NucleusController<DownloadPresenter>(),
DownloadAdapter.OnItemReleaseListener {
DownloadAdapter.DownloadItemListener {
/**
* Adapter containing the active downloads.
@ -110,6 +113,9 @@ class DownloadController : NucleusController<DownloadPresenter>(),
// Set clear button visibility.
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
// Set reorder button visibility.
menu.findItem(R.id.reorder).isVisible = !presenter.downloadQueue.isEmpty()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -124,6 +130,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)
}
else -> return super.onOptionsItemSelected(item)
}
return true
@ -264,4 +280,36 @@ class DownloadController : NucleusController<DownloadPresenter>(),
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)
}
}
}
}

View File

@ -1,9 +1,14 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import android.widget.PopupMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.setVectorCompat
import kotlinx.android.synthetic.main.download_item.*
import kotlinx.android.synthetic.main.download_item.migration_menu
/**
* Class used to hold the data of a download.
@ -12,10 +17,12 @@ import kotlinx.android.synthetic.main.download_item.*
* @param view the inflated view for this holder.
* @constructor creates a new download holder.
*/
class DownloadHolder(view: View, val adapter: DownloadAdapter) : BaseFlexibleViewHolder(view, adapter) {
class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
BaseFlexibleViewHolder(view, adapter) {
init {
setDragHandleView(reorder)
migration_menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
private lateinit var download: Download
@ -44,6 +51,10 @@ class DownloadHolder(view: View, val adapter: DownloadAdapter) : BaseFlexibleVie
notifyProgress()
notifyDownloadedPages()
}
migration_menu.setVectorCompat(
R.drawable.ic_more_vert_black_24dp, view.context
.getResourceColor(R.attr.icon_color))
}
@ -68,7 +79,34 @@ class DownloadHolder(view: View, val adapter: DownloadAdapter) : BaseFlexibleVie
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
adapter.onItemReleaseListener.onItemReleased(position)
adapter.downloadItemListener.onItemReleased(position)
}
private fun showPopupMenu(view: View) {
val item = adapter.getItem(adapterPosition) ?: return
// Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(view.context, view)
// Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.download_single, popup.menu)
val download = item.download
popup.menu.findItem(R.id.move_to_top).isVisible = adapterPosition != 0
popup.menu.findItem(R.id.move_to_bottom).isVisible = adapterPosition != adapter
.itemCount - 1
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem ->
adapter.downloadItemListener.onMenuItemClick(adapterPosition, menuItem)
true
}
// Finally show the PopupMenu
popup.show()
}
}

View File

@ -65,4 +65,9 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
downloadManager.reorderQueue(downloads)
}
fun cancelDownload(download: Download) {
downloadManager.deleteChapters(listOf(download.chapter), download.manga,
download.source)
}
}

View File

@ -26,7 +26,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxbinding.support.v4.view.pageSelections
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
import com.jakewharton.rxbinding.view.visible
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
@ -469,7 +468,7 @@ class LibraryController(
val showAll =
(selectedMangas.filter { (it as? LibraryManga)?.hide_title == true }).size == selectedMangas.size
menu.findItem(R.id.action_hide_title)?.title = activity?.getString(
if (showAll) R.string.label_show_title else R.string.label_hide_title
if (showAll) R.string.action_show_title else R.string.action_hide_title
)
}
}

View File

@ -78,7 +78,8 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle),
}
override fun getTitle(): String? {
return resources?.getString(R.string.migration)
return resources?.getString(R.string.migration) + " (${adapter?.items?.count { it.manga
.migrationStatus != MigrationStatus.RUNNUNG }}/${adapter?.itemCount ?: 0})"
}
override fun onViewCreated(view: View) {
@ -240,6 +241,12 @@ class MigrationListController(bundle: Bundle? = null) : BaseController(bundle),
}
}
override fun updateCount() {
launchUI {
setTitle()
}
}
override fun onDestroy() {
super.onDestroy()

View File

@ -38,9 +38,11 @@ class MigrationProcessAdapter(
fun enableButtons()
fun removeManga(item: MigrationProcessItem)
fun noMigration()
fun updateCount()
}
fun sourceFinished() {
menuItemListener.updateCount()
if (itemCount == 0) menuItemListener.noMigration()
if (allMangasDone()) menuItemListener.enableButtons()
}

View File

@ -1,60 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<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:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="@dimen/material_layout_keylines_screen_edge_margin"
android:paddingStart="0dp"
android:paddingTop="@dimen/material_component_lists_padding_above_list">
<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:contentDescription="@string/action_reorganize_by"
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="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toEndOf="@id/reorder"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Body1"
app:layout_constraintEnd_toStartOf="@+id/download_progress_text"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toTopOf="parent"
tools:text="Manga title" />
<TextView
android:id="@+id/chapter_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
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/migration_menu"
app:layout_constraintStart_toStartOf="@+id/manga_title"
app:layout_constraintTop_toBottomOf="@+id/manga_title"
tools:text="Chapter Title" />
<ProgressBar
android:id="@+id/download_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/migration_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_alignParentRight="true"
android:layout_toEndOf="@id/manga_title"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint"
tools:text="(0/10)"/>
<TextView
android:id="@+id/manga_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/download_progress_text"
android:layout_alignParentLeft="true"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginStart="@dimen/material_component_lists_single_line_with_avatar_height"
android:textAppearance="@style/TextAppearance.Regular.Body1"
tools:text="Manga title"/>
<TextView
android:id="@+id/chapter_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/manga_title"
android:maxLines="1"
android:ellipsize="end"
tools:text="Chapter Title"
android:layout_marginStart="@dimen/material_component_lists_single_line_with_avatar_height"
android:textAppearance="@style/TextAppearance.Regular.Caption"/>
<ProgressBar
android:id="@+id/download_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/chapter_title"
android:layout_marginStart="@dimen/material_component_lists_single_line_with_avatar_height"
style="?android:attr/progressBarStyleHorizontal"/>
app:layout_constraintBottom_toBottomOf="@+id/manga_title"
app:layout_constraintEnd_toStartOf="@+id/migration_menu"
app:layout_constraintTop_toTopOf="@+id/manga_title"
tools:text="(0/10)" />
<ImageView
android:id="@+id/reorder"
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
android:id="@+id/migration_menu"
android:layout_width="44dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
android:layout_gravity="start"
android:scaleType="center"
android:tint="?android:attr/textColorPrimary"
app:srcCompat="@drawable/ic_reorder_grey_24dp" />
android:layout_toEndOf="@id/download_progress_text"
android:contentDescription="@string/description_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_more_vert_black_24dp" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -19,4 +19,19 @@
android:visible="false"
app:showAsAction="never"/>
<item
android:id="@+id/reorder"
android:title="@string/action_reorganize_by"
android:visible="false"
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>
</menu>

View File

@ -0,0 +1,11 @@
<?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>

View File

@ -34,7 +34,7 @@
<item
android:id="@+id/action_reorganize"
android:title="@string/label_reorganize_by"
android:title="@string/action_reorganize_by"
app:showAsAction="never">
<menu>
<item
@ -42,7 +42,7 @@
android:title="@string/action_sort_alpha"/>
<item
android:id="@+id/action_alpha_dsc"
android:title="@string/label_alpha_reverse"/>
android:title="@string/action_alpha_reverse"/>
<item
android:id="@+id/action_update_asc"
android:title="@string/action_sort_last_updated"/>

View File

@ -32,7 +32,7 @@
<item
android:id="@+id/action_hide_title"
android:icon="@drawable/ic_swap_calls_white_24dp"
android:title="@string/label_hide_title"
android:title="@string/action_hide_title"
app:showAsAction="never" />
</menu>

View File

@ -22,10 +22,6 @@
<string name="label_selected">Selected: %1$d</string>
<string name="label_backup">Backup</string>
<string name="label_migration">Source migration</string>
<string name="label_reorganize_by">Reorder</string>
<string name="label_alpha_reverse">Alpha. (descending)</string>
<string name="label_hide_title">Hide title</string>
<string name="label_show_title">Show title</string>
<string name="label_extensions">Extensions</string>
<string name="label_extension_info">Extension info</string>
<string name="label_help">Help</string>
@ -44,8 +40,6 @@
<string name="action_sort_enabled">Enabled</string>
<string name="action_sort_total">Total chapters</string>
<string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_updated">Last updated</string>
<string name="action_sort_first_updated">First updated</string>
<string name="action_sort_drag_and_drop">Drag &amp; Drop</string>
<string name="action_search">Search</string>
<string name="action_skip_manga">Don\'t migrate</string>
@ -75,7 +69,7 @@
<string name="action_start">Start</string>
<string name="action_stop">Stop</string>
<string name="action_pause">Pause</string>
<string name="action_clear">Clear</string>
<string name="action_clear">Cancel all</string>
<string name="action_close">Close</string>
<string name="action_previous_chapter">Previous chapter</string>
<string name="action_next_chapter">Next chapter</string>
@ -111,6 +105,16 @@
<string name="action_search_manually">Search manually</string>
<string name="action_migrate_now">Migrate now</string>
<string name="action_copy_now">Copy now</string>
<string name="action_reorganize_by">Reorder</string>
<string name="action_alpha_reverse">Alpha. (descending)</string>
<string name="action_sort_last_updated">Last updated</string>
<string name="action_sort_first_updated">Last updated (desc.)</string>
<string name="action_hide_title">Hide title</string>
<string name="action_show_title">Show title</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>
<!-- Operations -->
<string name="deleting">Deleting…</string>