Browse Source

Added improvements for RecentChapters. Closes #320 (#324)

Bram van de Kerkhof 8 years ago
parent
commit
1fbec7bf3d

+ 181 - 34
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt

@@ -2,9 +2,10 @@ package eu.kanade.tachiyomi.ui.recent
 
 import android.os.Bundle
 import android.support.v4.app.DialogFragment
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.support.v7.view.ActionMode
+import android.view.*
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.MangaChapter
 import eu.kanade.tachiyomi.data.download.model.Download
@@ -13,6 +14,7 @@ import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.getResourceDrawable
+import eu.kanade.tachiyomi.util.toast
 import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
 import eu.kanade.tachiyomi.widget.DividerItemDecoration
 import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
@@ -22,15 +24,15 @@ import timber.log.Timber
 
 /**
  * Fragment that shows recent chapters.
- * Uses R.layout.fragment_recent_chapters.
+ * Uses [R.layout.fragment_recent_chapters].
  * UI related actions should be called from here.
  */
 @RequiresPresenter(RecentChaptersPresenter::class)
-class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), FlexibleViewHolder.OnListItemClickListener {
+class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
     companion object {
         /**
          * Create new RecentChaptersFragment.
-         *
+         * @return a new instance of [RecentChaptersFragment].
          */
         @JvmStatic
         fun newInstance(): RecentChaptersFragment {
@@ -38,6 +40,59 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
         }
     }
 
+    override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+        return false
+    }
+
+    /**
+     * Called when ActionMode item clicked
+     * @param mode the ActionMode object
+     * @param item item from ActionMode.
+     */
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
+            R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
+            R.id.action_download -> downloadChapters(getSelectedChapters())
+            R.id.action_delete -> {
+                MaterialDialog.Builder(activity)
+                        .content(R.string.confirm_delete_chapters)
+                        .positiveText(android.R.string.yes)
+                        .negativeText(android.R.string.no)
+                        .onPositive { dialog, action -> deleteChapters(getSelectedChapters()) }
+                        .show()
+            }
+            else -> return false
+        }
+        return true
+    }
+
+    /**
+     * Called when ActionMode created.
+     * @param mode the ActionMode object
+     * @param menu menu object of ActionMode
+     */
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
+        adapter.mode = FlexibleAdapter.MODE_MULTI
+        return true
+    }
+
+    /**
+     * Called when ActionMode destroyed
+     * @param mode the ActionMode object
+     */
+    override fun onDestroyActionMode(mode: ActionMode?) {
+        adapter.mode = FlexibleAdapter.MODE_SINGLE
+        adapter.clearSelection()
+        actionMode = null
+    }
+
+    /**
+     * Action mode for multiple selection.
+     */
+    private var actionMode: ActionMode? = null
+
     /**
      * Adapter containing the recent chapters.
      */
@@ -46,7 +101,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 
     /**
      * Called when view gets created
-     *
      * @param inflater layout inflater
      * @param container view group
      * @param savedState status of saved state
@@ -58,7 +112,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 
     /**
      * Called when view is created
-     *
      * @param view created view
      * @param savedInstanceState status of saved sate
      */
@@ -70,60 +123,108 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
         adapter = RecentChaptersAdapter(this)
         recycler.adapter = adapter
 
+        // Set swipe refresh listener
+        swipe_refresh.setOnRefreshListener { fetchChapters() }
+
         // Update toolbar text
         setToolbarTitle(R.string.label_recent_updates)
     }
 
+    /**
+     * Returns selected chapters
+     * @return list of [MangaChapter]s
+     */
+    fun getSelectedChapters(): List<MangaChapter> {
+        return adapter.selectedItems.map { adapter.getItem(it) as? MangaChapter }.filterNotNull()
+    }
+
     /**
      * Called when item in list is clicked
-     *
      * @param position position of clicked item
      */
     override fun onListItemClick(position: Int): Boolean {
         // Get item from position
         val item = adapter.getItem(position)
         if (item is MangaChapter) {
-            // Open chapter in reader
-            openChapter(item)
+            if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
+                toggleSelection(position)
+                return true
+            } else {
+                openChapter(item)
+                return false
+            }
         }
         return false
     }
 
     /**
      * Called when item in list is long clicked
-     *
      * @param position position of clicked item
      */
     override fun onListItemLongClick(position: Int) {
-        // Empty function
+        if (actionMode == null)
+            actionMode = activity.startSupportActionMode(this)
+
+        toggleSelection(position)
+    }
+
+    /**
+     * Called to toggle selection
+     * @param position position of selected item
+     */
+    private fun toggleSelection(position: Int) {
+        adapter.toggleSelection(position, false)
+
+        val count = adapter.selectedItemCount
+        if (count == 0) {
+            actionMode?.finish()
+        } else {
+            setContextTitle(count)
+            actionMode?.invalidate()
+        }
+    }
+
+    /**
+     * Set the context title
+     * @param count count of selected items
+     */
+    private fun setContextTitle(count: Int) {
+        actionMode?.title = getString(R.string.label_selected, count)
     }
 
     /**
      * Open chapter in reader
-     *
-     * @param chapter selected chapter
+     * @param mangaChapter selected [MangaChapter]
      */
-    private fun openChapter(chapter: MangaChapter) {
-        val intent = ReaderActivity.newIntent(activity, chapter.manga, chapter.chapter)
+    private fun openChapter(mangaChapter: MangaChapter) {
+        val intent = ReaderActivity.newIntent(activity, mangaChapter.manga, mangaChapter.chapter)
         startActivity(intent)
     }
 
+    /**
+     * Download selected items
+     * @param mangaChapters list of selected [MangaChapter]s
+     */
+    fun downloadChapters(mangaChapters: List<MangaChapter>) {
+        destroyActionModeIfNeeded()
+        presenter.downloadChapters(mangaChapters)
+    }
+
     /**
      * Populate adapter with chapters
-     *
-     * @param chapters list of chapters
+     * @param chapters list of [Any]
      */
     fun onNextMangaChapters(chapters: List<Any>) {
         (activity as MainActivity).updateEmptyView(chapters.isEmpty(),
                 R.string.information_no_recent, R.drawable.ic_history_black_128dp)
 
+        destroyActionModeIfNeeded()
         adapter.setItems(chapters)
     }
 
     /**
      * Update download status of chapter
-
-     * @param download download object containing download progress.
+     * @param download [Download] object containing download progress.
      */
     fun onChapterStatusChange(download: Download) {
         getHolder(download)?.onStatusChange(download.status)
@@ -132,8 +233,7 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 
     /**
      * Returns holder belonging to chapter
-     *
-     * @param download download object containing download progress.
+     * @param download [Download] object containing download progress.
      */
     private fun getHolder(download: Download): RecentChaptersHolder? {
         return recycler.findViewHolderForItemId(download.chapter.id) as? RecentChaptersHolder
@@ -141,28 +241,42 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 
     /**
      * Mark chapter as read
-     *
-     * @param item selected chapter with manga
+     * @param mangaChapters list of [MangaChapter] objects
      */
-    fun markAsRead(item: MangaChapter) {
-        presenter.markChapterRead(item.chapter, true)
+    fun markAsRead(mangaChapters: List<MangaChapter>) {
+        presenter.markChapterRead(mangaChapters, true)
         if (presenter.preferences.removeAfterMarkedAsRead()) {
-            deleteChapter(item)
+            deleteChapters(mangaChapters)
         }
     }
 
+    /**
+     * Delete selected chapters
+     * @param mangaChapters list of [MangaChapter] objects
+     */
+    fun deleteChapters(mangaChapters: List<MangaChapter>) {
+        destroyActionModeIfNeeded()
+        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
+        presenter.deleteChapters(mangaChapters)
+    }
+
+    /**
+     * Destory [ActionMode] if it's shown
+     */
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
     /**
      * Mark chapter as unread
-     *
-     * @param item selected chapter with manga
+     * @param mangaChapters list of selected [MangaChapter]
      */
-    fun markAsUnread(item: MangaChapter) {
-        presenter.markChapterRead(item.chapter, false)
+    fun markAsUnread(mangaChapters: List<MangaChapter>) {
+        presenter.markChapterRead(mangaChapters, false)
     }
 
     /**
      * Start downloading chapter
-     *
      * @param item selected chapter with manga
      */
     fun downloadChapter(item: MangaChapter) {
@@ -171,7 +285,6 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
 
     /**
      * Start deleting chapter
-     *
      * @param item selected chapter with manga
      */
     fun deleteChapter(item: MangaChapter) {
@@ -179,18 +292,52 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Flexib
         presenter.deleteChapter(item)
     }
 
+    /**
+     * Called when chapters are deleted
+     */
     fun onChaptersDeleted() {
         dismissDeletingDialog()
         adapter.notifyDataSetChanged()
     }
 
+    /**
+     * Called when error while deleting
+     * @param error error message
+     */
     fun onChaptersDeletedError(error: Throwable) {
         dismissDeletingDialog()
         Timber.e(error, error.message)
     }
 
+    /**
+     * Called to dismiss deleting dialog
+     */
     fun dismissDeletingDialog() {
         (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
     }
 
+    /**
+     * Called when swipe refresh activated.
+     */
+    fun fetchChapters() {
+        swipe_refresh.isRefreshing = true
+        presenter.fetchChaptersFromSource()
+    }
+
+    /**
+     * Called after refresh is completed
+     */
+    fun onFetchChaptersDone() {
+        swipe_refresh.isRefreshing = false
+    }
+
+    /**
+     * Called when something went wrong while refreshing
+     * @param error information on what went wrong
+     */
+    fun onFetchChaptersError(error: Throwable) {
+        swipe_refresh.isRefreshing = false
+        context.toast(error.message)
+    }
+
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt

@@ -122,8 +122,8 @@ class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapte
                 when (menuItem.itemId) {
                     R.id.action_download -> adapter.fragment.downloadChapter(it)
                     R.id.action_delete -> adapter.fragment.deleteChapter(it)
-                    R.id.action_mark_as_read -> adapter.fragment.markAsRead(it)
-                    R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(it)
+                    R.id.action_mark_as_read -> adapter.fragment.markAsRead(listOf(it))
+                    R.id.action_mark_as_unread -> adapter.fragment.markAsUnread(listOf(it))
                 }
                 true
             }

+ 56 - 21
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt

@@ -60,12 +60,16 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
         // Used to get recent chapters
         restartableLatestCache(GET_RECENT_CHAPTERS,
                 { getRecentChaptersObservable() },
-                { recentChaptersFragment, chapters ->
+                { fragment, chapters ->
                     // Update adapter to show recent manga's
-                    recentChaptersFragment.onNextMangaChapters(chapters)
+                    fragment.onNextMangaChapters(chapters)
                     // Update download status
                     updateChapterStatus(convertToMangaChaptersList(chapters))
-                }
+                    // Stop refresh
+                    fragment.onFetchChaptersDone()
+
+                },
+                { fragment, error -> fragment.onFetchChaptersError(error) }
         )
 
         // Used to update download status
@@ -88,7 +92,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Returns observable containing chapter status.
-
      * @return download object containing download progress.
      */
     private fun getChapterStatusObs(): Observable<Download> {
@@ -107,7 +110,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
     /**
      * Function to check if chapter is in recent list
      * @param chaptersId id of chapter
-     * *
      * @return exist in recent list
      */
     private fun chapterIdEquals(chaptersId: Long): Boolean {
@@ -121,9 +123,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Returns a list only containing MangaChapter objects.
-
      * @param input the list that will be converted.
-     * *
      * @return list containing MangaChapters objects.
      */
     private fun convertToMangaChaptersList(input: List<Any>): List<MangaChapter> {
@@ -144,7 +144,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Update status of chapters.
-
      * @param download download object containing progress.
      */
     private fun updateChapterStatus(download: Download) {
@@ -162,7 +161,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Update status of chapters
-
      * @param mangaChapters list containing recent chapters
      */
     private fun updateChapterStatus(mangaChapters: List<MangaChapter>) {
@@ -236,7 +234,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
     /**
      * Get date as time key
      * @param date desired date
-     * *
      * @return date as time key
      */
     private fun getMapKey(date: Long): Date {
@@ -251,26 +248,62 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Mark selected chapter as read
-     *
-     * @param chapter selected chapter
+     * @param mangaChapters list of selected MangaChapters
      * @param read read status
      */
-    fun markChapterRead(chapter: Chapter, read: Boolean) {
-        Observable.just(chapter)
-                .doOnNext { chapter ->
-                    chapter.read = read
+    fun markChapterRead(mangaChapters: List<MangaChapter>, read: Boolean) {
+        Observable.from(mangaChapters)
+                .doOnNext { mangaChapter ->
+                    mangaChapter.chapter.read = read
                     if (!read) {
-                        chapter.last_page_read = 0
+                        mangaChapter.chapter.last_page_read = 0
                     }
                 }
-                .flatMap { db.updateChapterProgress(it).asRxObservable() }
+                .map { mangaChapter -> mangaChapter.chapter }
+                .toList()
+                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
                 .subscribeOn(Schedulers.io())
                 .subscribe()
     }
 
+    /**
+     * Delete selected chapters
+     * @param chapters list of MangaChapters
+     */
+    fun deleteChapters(chapters: List<MangaChapter>) {
+        val wasRunning = downloadManager.isRunning
+        if (wasRunning) {
+            DownloadService.stop(context)
+        }
+        Observable.from(chapters)
+                .doOnNext { deleteChapter(it) }
+                .toList()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, result ->
+                    view.onChaptersDeleted()
+                    if (wasRunning) {
+                        DownloadService.start(context)
+                    }
+                }, { view, error ->
+                    view.onChaptersDeletedError(error)
+                })
+    }
+
+    /**
+     * Download selected chapters
+     * @param mangaChapters [MangaChapter] that is selected
+     */
+    fun downloadChapters(mangaChapters: List<MangaChapter>) {
+        DownloadService.start(context)
+        Observable.from(mangaChapters)
+                .doOnNext { downloadChapter(it) }
+                .subscribeOn(AndroidSchedulers.mainThread())
+                .subscribe()
+    }
+
     /**
      * Download selected chapter
-     *
      * @param item chapter that is selected
      */
     fun downloadChapter(item: MangaChapter) {
@@ -280,7 +313,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Delete selected chapter
-     *
      * @param item chapter that are selected
      */
     fun deleteChapter(item: MangaChapter) {
@@ -304,7 +336,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
     /**
      * Delete selected chapter
-     *
      * @param chapter chapter that is selected
      * @param manga manga that belongs to chapter
      */
@@ -315,4 +346,8 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
         chapter.status = Download.NOT_DOWNLOADED
     }
 
+    fun fetchChaptersFromSource() {
+        start(GET_RECENT_CHAPTERS)
+    }
+
 }

+ 8 - 1
app/src/main/res/layout/fragment_recent_chapters.xml

@@ -5,7 +5,13 @@
                 android:layout_height="match_parent"
                 android:orientation="vertical">
 
-    <android.support.v7.widget.RecyclerView
+    <android.support.v4.widget.SwipeRefreshLayout
+        android:id="@+id/swipe_refresh"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <android.support.v7.widget.RecyclerView
         android:id="@+id/recycler"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -13,5 +19,6 @@
         tools:listitem="@layout/item_recent_chapter">
 
     </android.support.v7.widget.RecyclerView>
+    </android.support.v4.widget.SwipeRefreshLayout>
 
 </RelativeLayout>

+ 3 - 2
app/src/main/res/layout/item_recent_chapter.xml

@@ -1,9 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout 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="fill_parent"
                 android:layout_height="?android:attr/listPreferredItemHeight"
-                xmlns:app="http://schemas.android.com/apk/res-auto">
+                android:background="?attr/selectable_list_drawable">
 
 
     <RelativeLayout
@@ -39,8 +40,8 @@
             android:layout_alignParentLeft="true"
             android:layout_alignParentStart="true"
             android:layout_centerVertical="true"
-            android:layout_marginRight="30dp"
             android:layout_marginEnd="30dp"
+            android:layout_marginRight="30dp"
             android:orientation="vertical">
 
             <TextView

+ 30 - 0
app/src/main/res/menu/chapter_recent_selection.xml

@@ -0,0 +1,30 @@
+<?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">
+
+    <item
+        android:id="@+id/action_download"
+        android:icon="@drawable/ic_file_download_white_24dp"
+        android:title="@string/action_download"
+        app:showAsAction="ifRoom"/>
+
+    <item
+        android:id="@+id/action_delete"
+        android:icon="@drawable/ic_delete_white_24dp"
+        android:title="@string/action_delete"
+        app:showAsAction="ifRoom"/>
+
+    <item
+        android:id="@+id/action_mark_as_read"
+        android:icon="@drawable/ic_done_all_white_24dp"
+        android:title="@string/action_mark_as_read"
+        app:showAsAction="ifRoom"/>
+
+    <item
+        android:id="@+id/action_mark_as_unread"
+        android:icon="@drawable/ic_done_all_grey_24dp"
+        android:title="@string/action_mark_as_unread"
+        app:showAsAction="ifRoom"/>
+
+</menu>