Browse Source

UI with Conductor (#784)

inorichi 8 years ago
parent
commit
2eeac0bf8b
100 changed files with 7376 additions and 5787 deletions
  1. 11 1
      app/build.gradle
  2. 0 8
      app/src/main/AndroidManifest.xml
  3. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt
  4. 0 13
      app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt
  5. 57 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
  6. 139 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java
  7. 3 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt
  8. 21 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
  9. 186 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java
  10. 92 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt
  11. 11 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt
  12. 10 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt
  13. 0 7
      app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt
  14. 0 20
      app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt
  15. 0 19
      app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt
  16. 1 5
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt
  17. 67 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
  18. 44 0
      app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java
  19. 558 609
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
  20. 14 5
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
  21. 18 36
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  22. 0 265
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt
  23. 29 12
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
  24. 321 0
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
  25. 47 0
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt
  26. 13 12
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
  27. 36 3
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
  28. 34 26
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt
  29. 86 0
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt
  30. 1 2
      app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt
  31. 33 0
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
  32. 0 29
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt
  33. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
  34. 48 0
      app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt
  35. 43 0
      app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt
  36. 87 87
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt
  37. 44 122
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
  38. 248 266
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
  39. 510 0
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
  40. 0 503
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt
  41. 49 49
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
  42. 28 27
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
  43. 70 0
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
  44. 56 56
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
  45. 2 3
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt
  46. 315 373
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
  47. 247 160
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  48. 74 0
      app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt
  49. 0 141
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt
  50. 186 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
  51. 0 5
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt
  52. 0 55
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
  53. 9 19
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt
  54. 56 49
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt
  55. 45 19
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt
  56. 470 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt
  57. 0 454
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt
  58. 415 446
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
  59. 32 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt
  60. 27 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt
  61. 42 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt
  62. 43 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt
  63. 43 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt
  64. 0 16
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt
  65. 0 16
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt
  66. 399 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt
  67. 0 393
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt
  68. 169 201
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
  69. 74 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt
  70. 80 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt
  71. 58 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt
  72. 44 33
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt
  73. 123 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt
  74. 0 173
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt
  75. 41 42
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt
  76. 6 8
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt
  77. 128 136
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt
  78. 46 46
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt
  79. 143 118
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt
  80. 13 36
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  81. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt
  82. 34 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt
  83. 27 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt
  84. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt
  85. 11 3
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt
  86. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt
  87. 322 339
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt
  88. 14 25
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt
  89. 24 33
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt
  90. 134 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt
  91. 0 139
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt
  92. 22 53
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt
  93. 43 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt
  94. 25 39
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt
  95. 56 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt
  96. 0 1
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt
  97. 23 0
      app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt
  98. 4 10
      app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt
  99. 281 0
      app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java
  100. 4 14
      app/src/main/res/layout/activity_edit_categories.xml

+ 11 - 1
app/build.gradle

@@ -100,6 +100,16 @@ android {
 
 dependencies {
 
+    compile "com.bluelinelabs:conductor:2.1.3"
+
+    final rxbindings_version = '1.0.1'
+    compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
+    compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
+    compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
+    compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
+
+    compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
+
     // Modified dependencies
     compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
     compile 'com.github.inorichi:junrar-android:634c1f5'
@@ -212,7 +222,7 @@ dependencies {
 }
 
 buildscript {
-    ext.kotlin_version = '1.1.1'
+    ext.kotlin_version = '1.1.2'
     repositories {
         mavenCentral()
     }

+ 0 - 8
app/src/main/AndroidManifest.xml

@@ -32,10 +32,6 @@
             <meta-data android:name="android.app.shortcuts"
                 android:resource="@xml/shortcuts"/>
         </activity>
-        <activity
-            android:name=".ui.manga.MangaActivity"
-            android:exported="true"
-            android:parentActivityName=".ui.main.MainActivity" />
         <activity
             android:name=".ui.reader.ReaderActivity"
             android:theme="@style/Theme.Reader" />
@@ -43,10 +39,6 @@
             android:name=".ui.setting.SettingsActivity"
             android:label="@string/label_settings"
             android:parentActivityName=".ui.main.MainActivity" />
-        <activity
-            android:name=".ui.category.CategoryActivity"
-            android:label="@string/label_categories"
-            android:parentActivityName=".ui.main.MainActivity" />
         <activity
             android:name=".widget.CustomLayoutPickerActivity"
             android:label="@string/app_name"

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt

@@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
  * @param chapter object containing chater
  * @param history      object containing history
  */
-class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
+data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)

+ 0 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt

@@ -1,7 +1,5 @@
 package eu.kanade.tachiyomi.ui.base.activity
 
-import android.os.Bundle
-import eu.kanade.tachiyomi.App
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.LocaleHelper
 import nucleus.view.NucleusAppCompatActivity
@@ -14,17 +12,6 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
         LocaleHelper.updateConfiguration(this)
     }
 
-    override fun onCreate(savedState: Bundle?) {
-        val superFactory = presenterFactory
-        setPresenterFactory {
-            superFactory.createPresenter().apply {
-                val app = application as App
-                context = app.applicationContext
-            }
-        }
-        super.onCreate(savedState)
-    }
-
     override fun getActivity() = this
 
     override fun onResume() {

+ 57 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt

@@ -0,0 +1,57 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.RestoreViewOnCreateController
+import com.bluelinelabs.conductor.Router
+
+abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
+        val view = inflateView(inflater, container)
+        onViewCreated(view, savedViewState)
+        return view
+    }
+
+    abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
+
+    open fun onViewCreated(view: View, savedViewState: Bundle?) { }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        if (type.isEnter) {
+            setTitle()
+        }
+        super.onChangeStarted(handler, type)
+    }
+
+    open fun getTitle(): String? {
+        return null
+    }
+
+    private fun setTitle() {
+        var parentController = parentController
+        while (parentController != null) {
+            if (parentController is BaseController && parentController.getTitle() != null) {
+                return
+            }
+            parentController = parentController.parentController
+        }
+
+        (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
+    }
+
+    fun Router.popControllerWithTag(tag: String): Boolean {
+        val controller = getControllerWithTag(tag)
+        if (controller != null) {
+            popController(controller)
+            return true
+        }
+        return false
+    }
+
+}

+ 139 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/DialogController.java

@@ -0,0 +1,139 @@
+package eu.kanade.tachiyomi.ui.base.controller;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.bluelinelabs.conductor.RestoreViewOnCreateController;
+import com.bluelinelabs.conductor.Router;
+import com.bluelinelabs.conductor.RouterTransaction;
+import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler;
+
+/**
+ * A controller that displays a dialog window, floating on top of its activity's window.
+ * This is a wrapper over {@link Dialog} object like {@link android.app.DialogFragment}.
+ *
+ * <p>Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog}
+ */
+public abstract class DialogController extends RestoreViewOnCreateController {
+
+    private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState";
+
+    private Dialog dialog;
+    private boolean dismissed;
+
+    /**
+     * Convenience constructor for use when no arguments are needed.
+     */
+    protected DialogController() {
+        super(null);
+    }
+
+    /**
+     * Constructor that takes arguments that need to be retained across restarts.
+     *
+     * @param args Any arguments that need to be retained.
+     */
+    protected DialogController(@Nullable Bundle args) {
+        super(args);
+    }
+
+    @NonNull
+    @Override
+    final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) {
+        dialog = onCreateDialog(savedViewState);
+        //noinspection ConstantConditions
+        dialog.setOwnerActivity(getActivity());
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                dismissDialog();
+            }
+        });
+        if (savedViewState != null) {
+            Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG);
+            if (dialogState != null) {
+                dialog.onRestoreInstanceState(dialogState);
+            }
+        }
+        return new View(getActivity());//stub view
+    }
+
+    @Override
+    protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
+        super.onSaveViewState(view, outState);
+        Bundle dialogState = dialog.onSaveInstanceState();
+        outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
+    }
+
+    @Override
+    protected void onAttach(@NonNull View view) {
+        super.onAttach(view);
+        dialog.show();
+    }
+
+    @Override
+    protected void onDetach(@NonNull View view) {
+        super.onDetach(view);
+        dialog.hide();
+    }
+
+    @Override
+    protected void onDestroyView(@NonNull View view) {
+        super.onDestroyView(view);
+        dialog.setOnDismissListener(null);
+        dialog.dismiss();
+        dialog = null;
+    }
+
+    /**
+     * Display the dialog, create a transaction and pushing the controller.
+     * @param router The router on which the transaction will be applied
+     */
+    public void showDialog(@NonNull Router router) {
+        showDialog(router, null);
+    }
+
+    /**
+     * Display the dialog, create a transaction and pushing the controller.
+     * @param router The router on which the transaction will be applied
+     * @param tag The tag for this controller
+     */
+    public void showDialog(@NonNull Router router, @Nullable String tag) {
+        dismissed = false;
+        router.pushController(RouterTransaction.with(this)
+                .pushChangeHandler(new SimpleSwapChangeHandler(false))
+                .popChangeHandler(new SimpleSwapChangeHandler(false))
+                .tag(tag));
+    }
+
+    /**
+     * Dismiss the dialog and pop this controller
+     */
+    public void dismissDialog() {
+        if (dismissed) {
+            return;
+        }
+        getRouter().popController(this);
+        dismissed = true;
+    }
+
+    @Nullable
+    protected Dialog getDialog() {
+        return dialog;
+    }
+
+    /**
+     * Build your own custom Dialog container such as an {@link android.app.AlertDialog}
+     *
+     * @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists.
+     * @return Return a new Dialog instance to be displayed by the Controller
+     */
+    @NonNull
+    protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState);
+}

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt

@@ -0,0 +1,3 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+interface NoToolbarElevationController

+ 21 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt

@@ -0,0 +1,21 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
+import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
+import nucleus.factory.PresenterFactory
+import nucleus.presenter.Presenter
+
+@Suppress("LeakingThis")
+abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
+        PresenterFactory<P> {
+
+    private val delegate = NucleusConductorDelegate(this)
+
+    val presenter: P
+        get() = delegate.presenter
+
+    init {
+        addLifecycleListener(NucleusConductorLifecycleListener(delegate))
+    }
+}

+ 186 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java

@@ -0,0 +1,186 @@
+package eu.kanade.tachiyomi.ui.base.controller;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.view.PagerAdapter;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.bluelinelabs.conductor.Controller;
+import com.bluelinelabs.conductor.Router;
+import com.bluelinelabs.conductor.RouterTransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An adapter for ViewPagers that uses Routers as pages
+ */
+public abstract class RouterPagerAdapter extends PagerAdapter {
+
+    private static final String KEY_SAVED_PAGES = "RouterPagerAdapter.savedStates";
+    private static final String KEY_MAX_PAGES_TO_STATE_SAVE = "RouterPagerAdapter.maxPagesToStateSave";
+    private static final String KEY_SAVE_PAGE_HISTORY = "RouterPagerAdapter.savedPageHistory";
+
+    private final Controller host;
+    private int maxPagesToStateSave = Integer.MAX_VALUE;
+    private SparseArray<Bundle> savedPages = new SparseArray<>();
+    private SparseArray<Router> visibleRouters = new SparseArray<>();
+    private ArrayList<Integer> savedPageHistory = new ArrayList<>();
+    private Router primaryRouter;
+
+    /**
+     * Creates a new RouterPagerAdapter using the passed host.
+     */
+    public RouterPagerAdapter(@NonNull Controller host) {
+        this.host = host;
+    }
+
+    /**
+     * Called when a router is instantiated. Here the router's root should be set if needed.
+     *
+     * @param router   The router used for the page
+     * @param position The page position to be instantiated.
+     */
+    public abstract void configureRouter(@NonNull Router router, int position);
+
+    /**
+     * Sets the maximum number of pages that will have their states saved. When this number is exceeded,
+     * the page that was state saved least recently will have its state removed from the save data.
+     */
+    public void setMaxPagesToStateSave(int maxPagesToStateSave) {
+        if (maxPagesToStateSave < 0) {
+            throw new IllegalArgumentException("Only positive integers may be passed for maxPagesToStateSave.");
+        }
+
+        this.maxPagesToStateSave = maxPagesToStateSave;
+
+        ensurePagesSaved();
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+        final String name = makeRouterName(container.getId(), getItemId(position));
+
+        Router router = host.getChildRouter(container, name);
+        if (!router.hasRootController()) {
+            Bundle routerSavedState = savedPages.get(position);
+
+            if (routerSavedState != null) {
+                router.restoreInstanceState(routerSavedState);
+                savedPages.remove(position);
+            }
+        }
+
+        router.rebindIfNeeded();
+        configureRouter(router, position);
+
+        if (router != primaryRouter) {
+            for (RouterTransaction transaction : router.getBackstack()) {
+                transaction.controller().setOptionsMenuHidden(true);
+            }
+        }
+
+        visibleRouters.put(position, router);
+        return router;
+    }
+
+    @Override
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        Router router = (Router)object;
+
+        Bundle savedState = new Bundle();
+        router.saveInstanceState(savedState);
+        savedPages.put(position, savedState);
+
+        savedPageHistory.remove((Integer)position);
+        savedPageHistory.add(position);
+
+        ensurePagesSaved();
+
+        host.removeChildRouter(router);
+
+        visibleRouters.remove(position);
+    }
+
+    @Override
+    public void setPrimaryItem(ViewGroup container, int position, Object object) {
+        Router router = (Router)object;
+        if (router != primaryRouter) {
+            if (primaryRouter != null) {
+                for (RouterTransaction transaction : primaryRouter.getBackstack()) {
+                    transaction.controller().setOptionsMenuHidden(true);
+                }
+            }
+            if (router != null) {
+                for (RouterTransaction transaction : router.getBackstack()) {
+                    transaction.controller().setOptionsMenuHidden(false);
+                }
+            }
+            primaryRouter = router;
+        }
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        Router router = (Router)object;
+        final List<RouterTransaction> backstack = router.getBackstack();
+        for (RouterTransaction transaction : backstack) {
+            if (transaction.controller().getView() == view) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Parcelable saveState() {
+        Bundle bundle = new Bundle();
+        bundle.putSparseParcelableArray(KEY_SAVED_PAGES, savedPages);
+        bundle.putInt(KEY_MAX_PAGES_TO_STATE_SAVE, maxPagesToStateSave);
+        bundle.putIntegerArrayList(KEY_SAVE_PAGE_HISTORY, savedPageHistory);
+        return bundle;
+    }
+
+    @Override
+    public void restoreState(Parcelable state, ClassLoader loader) {
+        Bundle bundle = (Bundle)state;
+        if (state != null) {
+            savedPages = bundle.getSparseParcelableArray(KEY_SAVED_PAGES);
+            maxPagesToStateSave = bundle.getInt(KEY_MAX_PAGES_TO_STATE_SAVE);
+            savedPageHistory = bundle.getIntegerArrayList(KEY_SAVE_PAGE_HISTORY);
+        }
+    }
+
+    /**
+     * Returns the already instantiated Router in the specified position or {@code null} if there
+     * is no router associated with this position.
+     */
+    @Nullable
+    public Router getRouter(int position) {
+        return visibleRouters.get(position);
+    }
+
+    public long getItemId(int position) {
+        return position;
+    }
+
+    SparseArray<Bundle> getSavedPages() {
+        return savedPages;
+    }
+
+    private void ensurePagesSaved() {
+        while (savedPages.size() > maxPagesToStateSave) {
+            int positionToRemove = savedPageHistory.remove(0);
+            savedPages.remove(positionToRemove);
+        }
+    }
+
+    private static String makeRouterName(int viewId, long id) {
+        return viewId + ":" + id;
+    }
+
+}

+ 92 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt

@@ -0,0 +1,92 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.os.Bundle
+import android.support.annotation.CallSuper
+import android.view.View
+import rx.Observable
+import rx.Subscription
+import rx.subscriptions.CompositeSubscription
+
+abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
+
+    var untilDetachSubscriptions = CompositeSubscription()
+        private set
+
+    var untilDestroySubscriptions = CompositeSubscription()
+        private set
+
+    @CallSuper
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        if (untilDetachSubscriptions.isUnsubscribed) {
+            untilDetachSubscriptions = CompositeSubscription()
+        }
+    }
+
+    @CallSuper
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+        untilDetachSubscriptions.unsubscribe()
+    }
+
+    @CallSuper
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        if (untilDestroySubscriptions.isUnsubscribed) {
+            untilDestroySubscriptions = CompositeSubscription()
+        }
+    }
+
+    @CallSuper
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        untilDestroySubscriptions.unsubscribe()
+    }
+
+
+    fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
+
+        return subscribe().also { untilDetachSubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
+
+        return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
+                                               onError: (Throwable) -> Unit): Subscription {
+
+        return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit,
+                                               onError: (Throwable) -> Unit,
+                                               onCompleted: () -> Unit): Subscription {
+
+        return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
+
+        return subscribe().also { untilDestroySubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
+
+        return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
+                                                onError: (Throwable) -> Unit): Subscription {
+
+        return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
+    }
+
+    fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit,
+                                                onError: (Throwable) -> Unit,
+                                                onCompleted: () -> Unit): Subscription {
+
+        return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
+    }
+
+}

+ 11 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt

@@ -0,0 +1,11 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.support.v4.widget.DrawerLayout
+import android.view.ViewGroup
+
+interface SecondaryDrawerController {
+
+    fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup?
+
+    fun cleanupSecondaryDrawer(drawer: DrawerLayout)
+}

+ 10 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt

@@ -0,0 +1,10 @@
+package eu.kanade.tachiyomi.ui.base.controller
+
+import android.support.design.widget.TabLayout
+
+interface TabbedController {
+
+    fun configureTabs(tabs: TabLayout) {}
+
+    fun cleanupTabs(tabs: TabLayout) {}
+}

+ 0 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt

@@ -1,7 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.fragment
-
-import android.support.v4.app.Fragment
-
-abstract class BaseFragment : Fragment(), FragmentMixin {
-
-}

+ 0 - 20
app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt

@@ -1,20 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.fragment
-
-import android.os.Bundle
-import eu.kanade.tachiyomi.App
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import nucleus.view.NucleusSupportFragment
-
-abstract class BaseRxFragment<P : BasePresenter<*>> : NucleusSupportFragment<P>(), FragmentMixin {
-
-    override fun onCreate(savedState: Bundle?) {
-        val superFactory = presenterFactory
-        setPresenterFactory {
-            superFactory.createPresenter().apply {
-                val app = activity.application as App
-                context = app.applicationContext
-            }
-        }
-        super.onCreate(savedState)
-    }
-}

+ 0 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt

@@ -1,19 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.fragment
-
-import android.support.v4.app.FragmentActivity
-import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
-
-interface FragmentMixin {
-
-    fun setToolbarTitle(title: String) {
-        (getActivity() as ActivityMixin).setToolbarTitle(title)
-    }
-
-    fun setToolbarTitle(resourceId: Int) {
-        (getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
-    }
-
-    fun getActivity(): FragmentActivity
-    
-    fun getString(resource: Int): String
-}

+ 1 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt

@@ -1,13 +1,9 @@
 package eu.kanade.tachiyomi.ui.base.presenter
 
-import android.content.Context
 import nucleus.presenter.RxPresenter
-import nucleus.view.ViewWithPresenter
 import rx.Observable
 
-open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
-
-    lateinit var context: Context
+open class BasePresenter<V> : RxPresenter<V>() {
 
     /**
      * Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle

+ 67 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java

@@ -0,0 +1,67 @@
+package eu.kanade.tachiyomi.ui.base.presenter;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+import nucleus.factory.PresenterFactory;
+import nucleus.presenter.Presenter;
+
+public class NucleusConductorDelegate<P extends Presenter> {
+
+    @Nullable private P presenter;
+    @Nullable private Bundle bundle;
+    private boolean presenterHasView = false;
+
+    private PresenterFactory<P> factory;
+
+    public NucleusConductorDelegate(PresenterFactory<P> creator) {
+        this.factory = creator;
+    }
+
+    public P getPresenter() {
+        if (presenter == null) {
+            presenter = factory.createPresenter();
+            presenter.create(bundle);
+        }
+        bundle = null;
+        return presenter;
+    }
+
+    Bundle onSaveInstanceState() {
+        Bundle bundle = new Bundle();
+        getPresenter();
+        if (presenter != null) {
+            presenter.save(bundle);
+        }
+        return bundle;
+    }
+
+    void onRestoreInstanceState(Bundle presenterState) {
+        if (presenter != null)
+            throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
+        bundle = presenterState;
+    }
+
+    void onTakeView(Object view) {
+        getPresenter();
+        if (presenter != null && !presenterHasView) {
+            //noinspection unchecked
+            presenter.takeView(view);
+            presenterHasView = true;
+        }
+    }
+
+    void onDropView() {
+        if (presenter != null && presenterHasView) {
+            presenter.dropView();
+            presenterHasView = false;
+        }
+    }
+
+    void onDestroy() {
+        if (presenter != null) {
+            presenter.destroy();
+            presenter = null;
+        }
+    }
+}

+ 44 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java

@@ -0,0 +1,44 @@
+package eu.kanade.tachiyomi.ui.base.presenter;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+import com.bluelinelabs.conductor.Controller;
+
+public class NucleusConductorLifecycleListener extends Controller.LifecycleListener {
+
+    private static final String PRESENTER_STATE_KEY = "presenter_state";
+
+    private NucleusConductorDelegate delegate;
+
+    public NucleusConductorLifecycleListener(NucleusConductorDelegate delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void postCreateView(@NonNull Controller controller, @NonNull View view) {
+        delegate.onTakeView(controller);
+    }
+
+    @Override
+    public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
+        delegate.onDropView();
+    }
+
+    @Override
+    public void preDestroy(@NonNull Controller controller) {
+        delegate.onDestroy();
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Controller controller, @NonNull Bundle outState) {
+        outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState());
+    }
+
+    @Override
+    public void onRestoreInstanceState(@NonNull Controller controller, @NonNull Bundle savedInstanceState) {
+        delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
+    }
+
+}

+ 558 - 609
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt → app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt

@@ -1,609 +1,558 @@
-package eu.kanade.tachiyomi.ui.catalogue
-
-import android.content.res.Configuration
-import android.os.Bundle
-import android.support.design.widget.Snackbar
-import android.support.v4.widget.DrawerLayout
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.widget.*
-import android.view.*
-import android.widget.ArrayAdapter
-import android.widget.ProgressBar
-import android.widget.Spinner
-import com.afollestad.materialdialogs.MaterialDialog
-import com.f2prateek.rx.preferences.Preference
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.util.connectivityManager
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.util.snack
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
-import kotlinx.android.synthetic.main.activity_main.*
-import kotlinx.android.synthetic.main.fragment_catalogue.*
-import kotlinx.android.synthetic.main.toolbar.*
-import nucleus.factory.RequiresPresenter
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.subjects.PublishSubject
-import uy.kohesive.injekt.injectLazy
-import java.util.concurrent.TimeUnit.MILLISECONDS
-
-/**
- * Fragment that shows the manga from the catalogue.
- * Uses R.layout.fragment_catalogue.
- */
-@RequiresPresenter(CataloguePresenter::class)
-open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener,
-        FlexibleAdapter.EndlessScrollListener<ProgressItem> {
-
-    /**
-     * Preferences helper.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * Spinner shown in the toolbar to change the selected source.
-     */
-    private var spinner: Spinner? = null
-
-    /**
-     * Adapter containing the list of manga from the catalogue.
-     */
-    private lateinit var adapter: FlexibleAdapter<IFlexible<*>>
-
-    /**
-     * Query of the search box.
-     */
-    private val query: String
-        get() = presenter.query
-
-    /**
-     * Selected index of the spinner (selected source).
-     */
-    private var selectedIndex: Int = 0
-
-    /**
-     * Time in milliseconds to wait for input events in the search query before doing network calls.
-     */
-    private val SEARCH_TIMEOUT = 1000L
-
-    /**
-     * Subject to debounce the query.
-     */
-    private val queryDebouncerSubject = PublishSubject.create<String>()
-
-    /**
-     * Subscription of the debouncer subject.
-     */
-    private var queryDebouncerSubscription: Subscription? = null
-
-    /**
-     * Subscription of the number of manga per row.
-     */
-    private var numColumnsSubscription: Subscription? = null
-
-    /**
-     * Search item.
-     */
-    private var searchItem: MenuItem? = null
-
-    /**
-     * Property to get the toolbar from the containing activity.
-     */
-    private val toolbar: Toolbar
-        get() = (activity as MainActivity).toolbar
-
-    /**
-     * Snackbar containing an error message when a request fails.
-     */
-    private var snack: Snackbar? = null
-
-    /**
-     * Navigation view containing filter items.
-     */
-    private var navView: CatalogueNavigationView? = null
-
-    /**
-     * Drawer listener to allow swipe only for closing the drawer.
-     */
-    private val drawerListener by lazy {
-        object : DrawerLayout.SimpleDrawerListener() {
-            override fun onDrawerClosed(drawerView: View) {
-                if (drawerView == navView) {
-                    activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
-                }
-            }
-
-            override fun onDrawerOpened(drawerView: View) {
-                if (drawerView == navView) {
-                    activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
-                }
-            }
-        }
-    }
-
-    lateinit var recycler: RecyclerView
-
-    private var progressItem: ProgressItem? = null
-
-    companion object {
-        /**
-         * Creates a new instance of this fragment.
-         *
-         * @return a new instance of [CatalogueFragment].
-         */
-        fun newInstance(): CatalogueFragment {
-            return CatalogueFragment()
-        }
-    }
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        setHasOptionsMenu(true)
-    }
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
-        return inflater.inflate(R.layout.fragment_catalogue, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedState: Bundle?) {
-        // Initialize adapter, scroll listener and recycler views
-        adapter = FlexibleAdapter(null, this)
-        setupRecycler()
-
-        // Create toolbar spinner
-        val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext ?: activity
-
-        val spinnerAdapter = ArrayAdapter(themedContext,
-                android.R.layout.simple_spinner_item, presenter.sources)
-        spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
-
-        val onItemSelected = IgnoreFirstSpinnerListener { position ->
-            val source = spinnerAdapter.getItem(position)
-            if (!presenter.isValidSource(source)) {
-                spinner?.setSelection(selectedIndex)
-                context.toast(R.string.source_requires_login)
-            } else if (source != presenter.source) {
-                selectedIndex = position
-                showProgressBar()
-                adapter.clear()
-                presenter.setActiveSource(source)
-                navView?.setFilters(presenter.filterItems)
-                activity.invalidateOptionsMenu()
-            }
-        }
-
-        selectedIndex = presenter.sources.indexOf(presenter.source)
-
-        spinner = Spinner(themedContext).apply {
-            adapter = spinnerAdapter
-            setSelection(selectedIndex)
-            onItemSelectedListener = onItemSelected
-        }
-
-        setToolbarTitle("")
-        toolbar.addView(spinner)
-
-        // Inflate and prepare drawer
-        val navView = activity.drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
-        this.navView = navView
-        activity.drawer.addView(navView)
-        activity.drawer.addDrawerListener(drawerListener)
-        navView.setFilters(presenter.filterItems)
-
-        navView.post {
-            if (isAdded && !activity.drawer.isDrawerOpen(navView))
-                activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
-        }
-
-        navView.onSearchClicked = {
-            val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
-            showProgressBar()
-            adapter.clear()
-            presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
-        }
-
-        navView.onResetClicked = {
-            presenter.appliedFilters = FilterList()
-            val newFilters = presenter.source.getFilterList()
-            presenter.sourceFilters = newFilters
-            navView.setFilters(presenter.filterItems)
-        }
-
-        showProgressBar()
-    }
-
-    private fun setupRecycler() {
-        if (!isAdded) return
-
-        numColumnsSubscription?.unsubscribe()
-
-        val oldRecycler = catalogue_view.getChildAt(1)
-        var oldPosition = RecyclerView.NO_POSITION
-        if (oldRecycler is RecyclerView) {
-            oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
-            oldRecycler.adapter = null
-
-            catalogue_view.removeView(oldRecycler)
-        }
-
-        recycler = if (presenter.isListMode) {
-            RecyclerView(context).apply {
-                layoutManager = LinearLayoutManager(context)
-                addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
-            }
-        } else {
-            (catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply {
-                numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
-                        .doOnNext { spanCount = it }
-                        .skip(1)
-                        // Set again the adapter to recalculate the covers height
-                        .subscribe { adapter = [email protected] }
-
-                (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
-                    override fun getSpanSize(position: Int): Int {
-                        return when (adapter?.getItemViewType(position)) {
-                            R.layout.item_catalogue_grid, null -> 1
-                            else -> spanCount
-                        }
-                    }
-                }
-            }
-        }
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-
-        catalogue_view.addView(recycler, 1)
-
-        if (oldPosition != RecyclerView.NO_POSITION) {
-            recycler.layoutManager.scrollToPosition(oldPosition)
-        }
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.catalogue_list, menu)
-
-        // Initialize search menu
-        searchItem = menu.findItem(R.id.action_search).apply {
-            val searchView = actionView as SearchView
-
-            if (!query.isBlank()) {
-                expandActionView()
-                searchView.setQuery(query, true)
-                searchView.clearFocus()
-            }
-            searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
-                override fun onQueryTextSubmit(query: String): Boolean {
-                    onSearchEvent(query, true)
-                    return true
-                }
-
-                override fun onQueryTextChange(newText: String): Boolean {
-                    onSearchEvent(newText, false)
-                    return true
-                }
-            })
-        }
-
-        // Setup filters button
-        menu.findItem(R.id.action_set_filter).apply {
-            icon.mutate()
-            if (presenter.sourceFilters.isEmpty()) {
-                isEnabled = false
-                icon.alpha = 128
-            } else {
-                isEnabled = true
-                icon.alpha = 255
-            }
-        }
-
-        // Show next display mode
-        menu.findItem(R.id.action_display_mode).apply {
-            val icon = if (presenter.isListMode)
-                R.drawable.ic_view_module_white_24dp
-            else
-                R.drawable.ic_view_list_white_24dp
-            setIcon(icon)
-        }
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_display_mode -> swapDisplayMode()
-            R.id.action_set_filter -> navView?.let { activity.drawer.openDrawer(Gravity.END) }
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    override fun onResume() {
-        super.onResume()
-        queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe { searchWithQuery(it) }
-    }
-
-    override fun onPause() {
-        queryDebouncerSubscription?.unsubscribe()
-        super.onPause()
-    }
-
-    override fun onDestroyView() {
-        navView?.let {
-            activity.drawer.removeDrawerListener(drawerListener)
-            activity.drawer.removeView(it)
-        }
-        numColumnsSubscription?.unsubscribe()
-        searchItem?.let {
-            if (it.isActionViewExpanded) it.collapseActionView()
-        }
-        spinner?.let { toolbar.removeView(it) }
-        super.onDestroyView()
-    }
-
-    /**
-     * Called when the input text changes or is submitted.
-     *
-     * @param query the new query.
-     * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
-     */
-    private fun onSearchEvent(query: String, now: Boolean) {
-        if (now) {
-            searchWithQuery(query)
-        } else {
-            queryDebouncerSubject.onNext(query)
-        }
-    }
-
-    /**
-     * Restarts the request with a new query.
-     *
-     * @param newQuery the new query.
-     */
-    private fun searchWithQuery(newQuery: String) {
-        // If text didn't change, do nothing
-        if (query == newQuery)
-            return
-
-        showProgressBar()
-        adapter.clear()
-
-        presenter.restartPager(newQuery)
-    }
-
-    /**
-     * Called from the presenter when the network request is received.
-     *
-     * @param page the current page.
-     * @param mangas the list of manga of the page.
-     */
-    fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
-        hideProgressBar()
-        if (page == 1) {
-            adapter.clear()
-            resetProgressItem()
-        }
-        adapter.onLoadMoreComplete(mangas)
-    }
-
-    /**
-     * Called from the presenter when the network request fails.
-     *
-     * @param error the error received.
-     */
-    fun onAddPageError(error: Throwable) {
-        adapter.onLoadMoreComplete(null)
-        hideProgressBar()
-
-        val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
-
-        snack?.dismiss()
-        snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) {
-            setAction(R.string.action_retry) {
-                // If not the first page, show bottom progress bar.
-                if (adapter.mainItemCount > 0) {
-                    val item = progressItem ?: return@setAction
-                    adapter.addScrollableFooterWithDelay(item, 0, true)
-                } else {
-                    showProgressBar()
-                }
-                presenter.requestNext()
-            }
-        }
-    }
-
-    /**
-     * Sets a new progress item and reenables the scroll listener.
-     */
-    private fun resetProgressItem() {
-        progressItem = ProgressItem()
-        adapter.endlessTargetCount = 0
-        adapter.setEndlessScrollListener(this, progressItem!!)
-    }
-
-    /**
-     * Called by the adapter when scrolled near the bottom.
-     */
-    override fun onLoadMore(lastPosition: Int, currentPage: Int) {
-        if (presenter.hasNextPage()) {
-            presenter.requestNext()
-        } else {
-            adapter.onLoadMoreComplete(null)
-            adapter.endlessTargetCount = 1
-        }
-    }
-
-    override fun noMoreLoad(newItemsSize: Int) {
-    }
-
-    /**
-     * Called from the presenter when a manga is initialized.
-     *
-     * @param manga the manga initialized
-     */
-    fun onMangaInitialized(manga: Manga) {
-        getHolder(manga)?.setImage(manga)
-    }
-
-    /**
-     * Swaps the current display mode.
-     */
-    fun swapDisplayMode() {
-        if (!isAdded) return
-
-        presenter.swapDisplayMode()
-        val isListMode = presenter.isListMode
-        activity.invalidateOptionsMenu()
-        setupRecycler()
-        if (!isListMode || !context.connectivityManager.isActiveNetworkMetered) {
-            // Initialize mangas if going to grid view or if over wifi when going to list view
-            val mangas = (0..adapter.itemCount-1).mapNotNull {
-                (adapter.getItem(it) as? CatalogueItem)?.manga
-            }
-            presenter.initializeMangas(mangas)
-        }
-    }
-
-    /**
-     * Returns a preference for the number of manga per row based on the current orientation.
-     *
-     * @return the preference.
-     */
-    fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
-        return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
-            presenter.prefs.portraitColumns()
-        else
-            presenter.prefs.landscapeColumns()
-    }
-
-    /**
-     * Returns the view holder for the given manga.
-     *
-     * @param manga the manga to find.
-     * @return the holder of the manga or null if it's not bound.
-     */
-    private fun getHolder(manga: Manga): CatalogueHolder? {
-        adapter.allBoundViewHolders.forEach { holder ->
-            val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
-            if (item != null && item.manga.id!! == manga.id!!) {
-                return holder as CatalogueHolder
-            }
-        }
-
-        return null
-    }
-
-    /**
-     * Shows the progress bar.
-     */
-    private fun showProgressBar() {
-        progress.visibility = ProgressBar.VISIBLE
-        snack?.dismiss()
-        snack = null
-    }
-
-    /**
-     * Hides active progress bars.
-     */
-    private fun hideProgressBar() {
-        progress.visibility = ProgressBar.GONE
-    }
-
-    /**
-     * Called when a manga is clicked.
-     *
-     * @param position the position of the element clicked.
-     * @return true if the item should be selected, false otherwise.
-     */
-    override fun onItemClick(position: Int): Boolean {
-        val item = adapter.getItem(position) as? CatalogueItem ?: return false
-
-        val intent = MangaActivity.newIntent(activity, item.manga, true)
-        startActivity(intent)
-        return false
-    }
-
-    /**
-     * Called when a manga is long clicked.
-     *
-     * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
-     * in, the list consists of the default category plus the user's categories. The default category is preselected on
-     * new manga, and on already favorited manga the manga's categories are preselected.
-     *
-     * @param position the position of the element clicked.
-     */
-    override fun onItemLongClick(position: Int) {
-        // Get manga
-        val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return
-        // Fetch categories
-        val categories = presenter.getCategories()
-
-        if (manga.favorite){
-            MaterialDialog.Builder(activity)
-                    .items(getString(R.string.remove_from_library ))
-                    .itemsCallback { _, _, which, _ ->
-                        when (which) {
-                            0 -> {
-                                presenter.changeMangaFavorite(manga)
-                                adapter.notifyItemChanged(position)
-                            }
-                        }
-                    }.show()
-        }else{
-            val defaultCategory = categories.find { it.id == preferences.defaultCategory()}
-            if(defaultCategory != null) {
-                presenter.changeMangaFavorite(manga)
-                presenter.moveMangaToCategory(defaultCategory, manga)
-                // Show manga has been added
-                context.toast(R.string.added_to_library)
-                adapter.notifyItemChanged(position)
-            } else {
-                MaterialDialog.Builder(activity)
-                        .title(R.string.action_move_category)
-                        .items(categories.map { it.name })
-                        .itemsCallbackMultiChoice(presenter.getMangaCategoryIds(manga)) { dialog, position, _ ->
-                            if (position.contains(0) && position.count() > 1) {
-                                // Deselect default category
-                                dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray())
-                                dialog.context.toast(R.string.invalid_combination)
-                            }
-                            true
-                        }
-                        .alwaysCallMultiChoiceCallback()
-                        .positiveText(android.R.string.ok)
-                        .negativeText(android.R.string.cancel)
-                        .onPositive { dialog, _ ->
-                            val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList()
-                            updateMangaCategories(manga, selectedCategories, position)
-                        }
-                        .build()
-                        .show()
-            }
-        }
-    }
-
-    /**
-     * Update manga to use selected categories.
-     *
-     * @param manga needed to change
-     * @param selectedCategories selected categories
-     * @param position position of adapter
-     */
-    private fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>, position: Int) {
-        presenter.updateMangaCategories(manga,selectedCategories)
-        adapter.notifyItemChanged(position)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.catalogue
+
+import android.content.res.Configuration
+import android.os.Bundle
+import android.support.design.widget.Snackbar
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.*
+import android.view.*
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.f2prateek.rx.preferences.Preference
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
+import com.jakewharton.rxbinding.widget.itemSelections
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
+import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.*
+import eu.kanade.tachiyomi.widget.AutofitRecyclerView
+import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.fragment_catalogue.view.*
+import kotlinx.android.synthetic.main.toolbar.*
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.subscriptions.Subscriptions
+import timber.log.Timber
+import uy.kohesive.injekt.injectLazy
+import java.util.concurrent.TimeUnit
+
+/**
+ * Controller to manage the catalogues available in the app.
+ */
+open class CatalogueController(bundle: Bundle? = null) :
+        NucleusController<CataloguePresenter>(bundle),
+        SecondaryDrawerController,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        FlexibleAdapter.EndlessScrollListener<ProgressItem>,
+        ChangeMangaCategoriesDialog.Listener {
+
+    /**
+     * Preferences helper.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    /**
+     * Adapter containing the list of manga from the catalogue.
+     */
+    private var adapter: FlexibleAdapter<IFlexible<*>>? = null
+
+    /**
+     * Spinner shown in the toolbar to change the selected source.
+     */
+    private var spinner: Spinner? = null
+
+    /**
+     * Snackbar containing an error message when a request fails.
+     */
+    private var snack: Snackbar? = null
+
+    /**
+     * Navigation view containing filter items.
+     */
+    private var navView: CatalogueNavigationView? = null
+
+    /**
+     * Recycler view with the list of results.
+     */
+    private var recycler: RecyclerView? = null
+
+    private var drawerListener: DrawerLayout.DrawerListener? = null
+
+    /**
+     * Query of the search box.
+     */
+    private val query: String
+        get() = presenter.query
+
+    /**
+     * Selected index of the spinner (selected source).
+     */
+    private var selectedIndex: Int = 0
+
+    /**
+     * Subscription for the search view.
+     */
+    private var searchViewSubscription: Subscription? = null
+
+    private var numColumnsSubscription: Subscription? = null
+
+    private var progressItem: ProgressItem? = null
+
+    init {
+        setHasOptionsMenu(true)
+    }
+
+    override fun getTitle(): String? {
+        return ""
+    }
+
+    override fun createPresenter(): CataloguePresenter {
+        return CataloguePresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_catalogue, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        // Initialize adapter, scroll listener and recycler views
+        adapter = FlexibleAdapter(null, this)
+        setupRecycler(view)
+
+        // Create toolbar spinner
+        val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
+                ?: activity
+
+        val spinnerAdapter = ArrayAdapter(themedContext,
+                android.R.layout.simple_spinner_item, presenter.sources)
+        spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
+
+        val onItemSelected: (Int) -> Unit = { position ->
+            val source = spinnerAdapter.getItem(position)
+            if (!presenter.isValidSource(source)) {
+                spinner?.setSelection(selectedIndex)
+                activity?.toast(R.string.source_requires_login)
+            } else if (source != presenter.source) {
+                selectedIndex = position
+                showProgressBar()
+                adapter?.clear()
+                presenter.setActiveSource(source)
+                navView?.setFilters(presenter.filterItems)
+                activity?.invalidateOptionsMenu()
+            }
+        }
+
+        selectedIndex = presenter.sources.indexOf(presenter.source)
+
+        spinner = Spinner(themedContext).apply {
+            adapter = spinnerAdapter
+            setSelection(selectedIndex)
+            itemSelections()
+                    .skip(1)
+                    .filter { it != AdapterView.INVALID_POSITION }
+                    .subscribeUntilDestroy { onItemSelected(it) }
+        }
+
+        activity?.toolbar?.addView(spinner)
+
+        view.progress?.visible()
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        activity?.toolbar?.removeView(spinner)
+        numColumnsSubscription?.unsubscribe()
+        numColumnsSubscription = null
+        searchViewSubscription = null
+        adapter = null
+        spinner = null
+        snack = null
+        recycler = null
+    }
+
+    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
+        // Inflate and prepare drawer
+        val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
+        this.navView = navView
+        drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
+            drawer.addDrawerListener(it)
+        }
+        navView.setFilters(presenter.filterItems)
+
+        navView.post {
+            if (isAttached && !drawer.isDrawerOpen(navView))
+                drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
+        }
+
+        navView.onSearchClicked = {
+            val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
+            showProgressBar()
+            adapter?.clear()
+            presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
+        }
+
+        navView.onResetClicked = {
+            presenter.appliedFilters = FilterList()
+            val newFilters = presenter.source.getFilterList()
+            presenter.sourceFilters = newFilters
+            navView.setFilters(presenter.filterItems)
+        }
+        return navView
+    }
+
+    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
+        drawerListener?.let { drawer.removeDrawerListener(it) }
+        drawerListener = null
+        navView = null
+    }
+
+    private fun setupRecycler(view: View) {
+        numColumnsSubscription?.unsubscribe()
+
+        var oldPosition = RecyclerView.NO_POSITION
+            val oldRecycler = view.catalogue_view?.getChildAt(1)
+            if (oldRecycler is RecyclerView) {
+                oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
+                oldRecycler.adapter = null
+
+                view.catalogue_view?.removeView(oldRecycler)
+            }
+
+        val recycler = if (presenter.isListMode) {
+            RecyclerView(view.context).apply {
+                layoutManager = LinearLayoutManager(context)
+                addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
+            }
+        } else {
+            (view.catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply {
+                numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
+                        .doOnNext { spanCount = it }
+                        .skip(1)
+                        // Set again the adapter to recalculate the covers height
+                        .subscribe { adapter = [email protected] }
+
+                (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+                    override fun getSpanSize(position: Int): Int {
+                        return when (adapter?.getItemViewType(position)) {
+                            R.layout.item_catalogue_grid, null -> 1
+                            else -> spanCount
+                        }
+                    }
+                }
+            }
+        }
+        recycler.setHasFixedSize(true)
+        recycler.adapter = adapter
+
+        view.catalogue_view.addView(recycler, 1)
+
+        if (oldPosition != RecyclerView.NO_POSITION) {
+            recycler.layoutManager.scrollToPosition(oldPosition)
+        }
+        this.recycler = recycler
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.catalogue_list, menu)
+
+        // Initialize search menu
+        menu.findItem(R.id.action_search).apply {
+            val searchView = actionView as SearchView
+
+            if (!query.isBlank()) {
+                expandActionView()
+                searchView.setQuery(query, true)
+                searchView.clearFocus()
+            }
+
+            val searchEventsObservable = searchView.queryTextChangeEvents()
+                    .skip(1)
+                    .share()
+            val writingObservable = searchEventsObservable
+                    .filter { !it.isSubmitted }
+                    .debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
+            val submitObservable = searchEventsObservable
+                    .filter { it.isSubmitted }
+
+            searchViewSubscription?.unsubscribe()
+            searchViewSubscription = Observable.merge(writingObservable, submitObservable)
+                    .map { it.queryText().toString() }
+                    .distinctUntilChanged()
+                    .subscribeUntilDestroy { searchWithQuery(it) }
+
+            untilDestroySubscriptions.add(
+                    Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
+        }
+
+        // Setup filters button
+        menu.findItem(R.id.action_set_filter).apply {
+            icon.mutate()
+            if (presenter.sourceFilters.isEmpty()) {
+                isEnabled = false
+                icon.alpha = 128
+            } else {
+                isEnabled = true
+                icon.alpha = 255
+            }
+        }
+
+        // Show next display mode
+        menu.findItem(R.id.action_display_mode).apply {
+            val icon = if (presenter.isListMode)
+                R.drawable.ic_view_module_white_24dp
+            else
+                R.drawable.ic_view_list_white_24dp
+            setIcon(icon)
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_display_mode -> swapDisplayMode()
+            R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    /**
+     * Restarts the request with a new query.
+     *
+     * @param newQuery the new query.
+     */
+    private fun searchWithQuery(newQuery: String) {
+        // If text didn't change, do nothing
+        if (query == newQuery)
+            return
+
+        showProgressBar()
+        adapter?.clear()
+
+        presenter.restartPager(newQuery)
+    }
+
+    /**
+     * Called from the presenter when the network request is received.
+     *
+     * @param page the current page.
+     * @param mangas the list of manga of the page.
+     */
+    fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
+        val adapter = adapter ?: return
+        hideProgressBar()
+        if (page == 1) {
+            adapter.clear()
+            resetProgressItem()
+        }
+        adapter.onLoadMoreComplete(mangas)
+    }
+
+    /**
+     * Called from the presenter when the network request fails.
+     *
+     * @param error the error received.
+     */
+    fun onAddPageError(error: Throwable) {
+        Timber.e(error)
+        val adapter = adapter ?: return
+        adapter.onLoadMoreComplete(null)
+        hideProgressBar()
+
+        val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
+
+        snack?.dismiss()
+        snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
+            setAction(R.string.action_retry) {
+                // If not the first page, show bottom progress bar.
+                if (adapter.mainItemCount > 0) {
+                    val item = progressItem ?: return@setAction
+                    adapter.addScrollableFooterWithDelay(item, 0, true)
+                } else {
+                    showProgressBar()
+                }
+                presenter.requestNext()
+            }
+        }
+    }
+
+    /**
+     * Sets a new progress item and reenables the scroll listener.
+     */
+    private fun resetProgressItem() {
+        progressItem = ProgressItem()
+        adapter?.endlessTargetCount = 0
+        adapter?.setEndlessScrollListener(this, progressItem!!)
+    }
+
+    /**
+     * Called by the adapter when scrolled near the bottom.
+     */
+    override fun onLoadMore(lastPosition: Int, currentPage: Int) {
+        Timber.e("onLoadMore")
+        if (presenter.hasNextPage()) {
+            presenter.requestNext()
+        } else {
+            adapter?.onLoadMoreComplete(null)
+            adapter?.endlessTargetCount = 1
+        }
+    }
+
+    override fun noMoreLoad(newItemsSize: Int) {
+    }
+
+    /**
+     * Called from the presenter when a manga is initialized.
+     *
+     * @param manga the manga initialized
+     */
+    fun onMangaInitialized(manga: Manga) {
+        getHolder(manga)?.setImage(manga)
+    }
+
+    /**
+     * Swaps the current display mode.
+     */
+    fun swapDisplayMode() {
+        val view = view ?: return
+        val adapter = adapter ?: return
+
+        presenter.swapDisplayMode()
+        val isListMode = presenter.isListMode
+        activity?.invalidateOptionsMenu()
+        setupRecycler(view)
+        if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
+            // Initialize mangas if going to grid view or if over wifi when going to list view
+            val mangas = (0..adapter.itemCount-1).mapNotNull {
+                (adapter.getItem(it) as? CatalogueItem)?.manga
+            }
+            presenter.initializeMangas(mangas)
+        }
+    }
+
+    /**
+     * Returns a preference for the number of manga per row based on the current orientation.
+     *
+     * @return the preference.
+     */
+    fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
+        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
+            presenter.prefs.portraitColumns()
+        else
+            presenter.prefs.landscapeColumns()
+    }
+
+    /**
+     * Returns the view holder for the given manga.
+     *
+     * @param manga the manga to find.
+     * @return the holder of the manga or null if it's not bound.
+     */
+    private fun getHolder(manga: Manga): CatalogueHolder? {
+        val adapter = adapter ?: return null
+
+        adapter.allBoundViewHolders.forEach { holder ->
+            val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
+            if (item != null && item.manga.id!! == manga.id!!) {
+                return holder as CatalogueHolder
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * Shows the progress bar.
+     */
+    private fun showProgressBar() {
+        view?.progress?.visible()
+        snack?.dismiss()
+        snack = null
+    }
+
+    /**
+     * Hides active progress bars.
+     */
+    private fun hideProgressBar() {
+        view?.progress?.gone()
+    }
+
+    /**
+     * Called when a manga is clicked.
+     *
+     * @param position the position of the element clicked.
+     * @return true if the item should be selected, false otherwise.
+     */
+    override fun onItemClick(position: Int): Boolean {
+        val item = adapter?.getItem(position) as? CatalogueItem ?: return false
+        router.pushController(RouterTransaction.with(MangaController(item.manga, true))
+                .pushChangeHandler(FadeChangeHandler())
+                .popChangeHandler(FadeChangeHandler()))
+
+        return false
+    }
+
+    /**
+     * Called when a manga is long clicked.
+     *
+     * Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
+     * in, the list consists of the default category plus the user's categories. The default category is preselected on
+     * new manga, and on already favorited manga the manga's categories are preselected.
+     *
+     * @param position the position of the element clicked.
+     */
+    override fun onItemLongClick(position: Int) {
+        val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
+        if (manga.favorite) {
+            MaterialDialog.Builder(activity!!)
+                    .items(resources?.getString(R.string.remove_from_library))
+                    .itemsCallback { _, _, which, _ ->
+                        when (which) {
+                            0 -> {
+                                presenter.changeMangaFavorite(manga)
+                                adapter?.notifyItemChanged(position)
+                            }
+                        }
+                    }.show()
+        } else {
+            presenter.changeMangaFavorite(manga)
+            adapter?.notifyItemChanged(position)
+
+            val categories = presenter.getCategories()
+            val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
+            if (defaultCategory != null) {
+                presenter.moveMangaToCategory(manga, defaultCategory)
+            } else if (categories.size <= 1) { // default or the one from the user
+                presenter.moveMangaToCategory(manga, categories.firstOrNull())
+            } else {
+                val ids = presenter.getMangaCategoryIds(manga)
+                val preselected = ids.mapNotNull { id ->
+                    categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
+                }.toTypedArray()
+
+                ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
+                        .showDialog(router)
+            }
+        }
+
+    }
+
+    /**
+     * Update manga to use selected categories.
+     *
+     * @param mangas The list of manga to move to categories.
+     * @param categories The list of categories where manga will be placed.
+     */
+    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
+        val manga = mangas.firstOrNull() ?: return
+        presenter.updateMangaCategories(manga, categories)
+    }
+
+}

+ 14 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt

@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.ui.catalogue
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.widget.FrameLayout
 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.database.models.Manga
 import eu.kanade.tachiyomi.util.inflate
@@ -19,11 +19,16 @@ class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>()
         return R.layout.item_catalogue_grid
     }
 
-    override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder {
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
+                                  parent: ViewGroup): CatalogueHolder {
+
         if (parent is AutofitRecyclerView) {
             val view = parent.inflate(R.layout.item_catalogue_grid).apply {
-                card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
-                gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
+                card.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, parent.itemWidth / 3 * 4)
+                gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
             }
             return CatalogueGridHolder(view, adapter)
         } else {
@@ -32,7 +37,11 @@ class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>()
         }
     }
 
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: CatalogueHolder, position: Int, payloads: List<Any?>?) {
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: CatalogueHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
         holder.onSetValues(manga)
     }
 

+ 18 - 36
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -25,32 +25,18 @@ import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import rx.subjects.PublishSubject
 import timber.log.Timber
-import uy.kohesive.injekt.injectLazy
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
 /**
- * Presenter of [CatalogueFragment].
+ * Presenter of [CatalogueController].
  */
-open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
-
-    /**
-     * Source manager.
-     */
-    val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Database.
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Preferences.
-     */
-    val prefs: PreferencesHelper by injectLazy()
-
-    /**
-     * Cover cache.
-     */
-    val coverCache: CoverCache by injectLazy()
+open class CataloguePresenter(
+        val sourceManager: SourceManager = Injekt.get(),
+        val db: DatabaseHelper = Injekt.get(),
+        val prefs: PreferencesHelper = Injekt.get(),
+        val coverCache: CoverCache = Injekt.get()
+) : BasePresenter<CatalogueController>() {
 
     /**
      * Enabled sources.
@@ -182,7 +168,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         pageSubscription = Observable.defer { pager.requestNext() }
                 .subscribeFirst({ view, page ->
                     // Nothing to do when onNext is emitted.
-                }, CatalogueFragment::onAddPageError)
+                }, CatalogueController::onAddPageError)
     }
 
     /**
@@ -404,7 +390,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * @return List of categories, default plus user categories
      */
     fun getCategories(): List<Category> {
-        return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
+        return db.getCategories().executeAsBlocking()
     }
 
     /**
@@ -415,10 +401,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      */
     fun getMangaCategoryIds(manga: Manga): Array<Int?> {
         val categories = db.getCategoriesForManga(manga).executeAsBlocking()
-        if (categories.isEmpty()) {
-            return arrayListOf(Category.createDefault().id).toTypedArray()
-        }
-        return categories.map { it.id }.toTypedArray()
+        return categories.mapNotNull { it.id }.toTypedArray()
     }
 
     /**
@@ -427,10 +410,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * @param categories the selected categories.
      * @param manga the manga to move.
      */
-    fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
-        val mc = categories.map { MangaCategory.create(manga, it) }
-
-        db.setMangaCategories(mc, arrayListOf(manga))
+    fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
+        val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
+        db.setMangaCategories(mc, listOf(manga))
     }
 
     /**
@@ -439,8 +421,8 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * @param category the selected category.
      * @param manga the manga to move.
      */
-    fun moveMangaToCategory(category: Category, manga: Manga) {
-        moveMangaToCategories(arrayListOf(category), manga)
+    fun moveMangaToCategory(manga: Manga, category: Category?) {
+        moveMangaToCategories(manga, listOfNotNull(category))
     }
 
     /**
@@ -454,7 +436,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
             if (!manga.favorite)
                 changeMangaFavorite(manga)
 
-            moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
+            moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
         } else {
             changeMangaFavorite(manga)
         }

+ 0 - 265
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt

@@ -1,265 +0,0 @@
-package eu.kanade.tachiyomi.ui.category
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import com.afollestad.materialdialogs.MaterialDialog
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.helpers.UndoHelper
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
-import kotlinx.android.synthetic.main.activity_edit_categories.*
-import kotlinx.android.synthetic.main.toolbar.*
-import nucleus.factory.RequiresPresenter
-
-
-/**
- * Activity that shows categories.
- * Uses R.layout.activity_edit_categories.
- * UI related actions should be called from here.
- */
-@RequiresPresenter(CategoryPresenter::class)
-class CategoryActivity :
-        BaseRxActivity<CategoryPresenter>(),
-        ActionMode.Callback,
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener,
-        UndoHelper.OnUndoListener {
-
-    /**
-     * Object used to show actionMode toolbar.
-     */
-    var actionMode: ActionMode? = null
-
-    /**
-     * Adapter containing category items.
-     */
-    private lateinit var adapter: CategoryAdapter
-
-    companion object {
-        /**
-         * Create new CategoryActivity intent.
-         *
-         * @param context context information.
-         */
-        fun newIntent(context: Context): Intent {
-            return Intent(context, CategoryActivity::class.java)
-        }
-    }
-
-    override fun onCreate(savedState: Bundle?) {
-        setAppTheme()
-        super.onCreate(savedState)
-
-        // Inflate activity_edit_categories.xml.
-        setContentView(R.layout.activity_edit_categories)
-
-        // Setup the toolbar.
-        setupToolbar(toolbar)
-
-        // Get new adapter.
-        adapter = CategoryAdapter(this)
-
-        // Create view and inject category items into view
-        recycler.layoutManager = LinearLayoutManager(this)
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-
-        adapter.isHandleDragEnabled = true
-
-        // Create OnClickListener for creating new category
-        fab.setOnClickListener {
-            MaterialDialog.Builder(this)
-                    .title(R.string.action_add_category)
-                    .negativeText(android.R.string.cancel)
-                    .input(R.string.name, 0, false)
-                    { dialog, input -> presenter.createCategory(input.toString()) }
-                    .show()
-        }
-    }
-
-    /**
-     * Fill adapter with category items
-     *
-     * @param categories list containing categories
-     */
-    fun setCategories(categories: List<CategoryItem>) {
-        actionMode?.finish()
-        adapter.updateDataSet(categories.toMutableList())
-        val selected = categories.filter { it.isSelected }
-        if (selected.isNotEmpty()) {
-            selected.forEach { onItemLongClick(categories.indexOf(it)) }
-        }
-    }
-
-    /**
-     * Show MaterialDialog which let user change category name.
-     *
-     * @param category category that will be edited.
-     */
-    private fun editCategory(category: Category) {
-        MaterialDialog.Builder(this)
-                .title(R.string.action_rename_category)
-                .negativeText(android.R.string.cancel)
-                .input(getString(R.string.name), category.name, false)
-                { dialog, input -> presenter.renameCategory(category, input.toString()) }
-                .show()
-    }
-
-    /**
-     * Called when action mode item clicked.
-     *
-     * @param actionMode action mode toolbar.
-     * @param menuItem selected menu item.
-     *
-     * @return action mode item clicked exist status
-     */
-    override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
-        when (menuItem.itemId) {
-            R.id.action_delete -> {
-                UndoHelper(adapter, this)
-                        .withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
-                            override fun onPreAction(): Boolean {
-                                adapter.selectedPositions.forEach { adapter.getItem(it).isSelected = false }
-                                return false
-                            }
-
-                            override fun onPostAction() {
-                                actionMode.finish()
-                            }
-                        })
-                        .remove(adapter.selectedPositions, recycler.parent as View,
-                                R.string.snack_categories_deleted, R.string.action_undo, 3000)
-            }
-            R.id.action_edit -> {
-                // Edit selected category
-                if (adapter.selectedItemCount == 1) {
-                    val position = adapter.selectedPositions.first()
-                    editCategory(adapter.getItem(position).category)
-                }
-            }
-            else -> return false
-        }
-        return true
-    }
-
-    /**
-     * Inflate menu when action mode selected.
-     *
-     * @param mode ActionMode object
-     * @param menu Menu object
-     *
-     * @return true
-     */
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        // Inflate menu.
-        mode.menuInflater.inflate(R.menu.category_selection, menu)
-        // Enable adapter multi selection.
-        adapter.mode = FlexibleAdapter.MODE_MULTI
-        return true
-    }
-
-    /**
-     * Called each time the action mode is shown.
-     * Always called after onCreateActionMode
-     *
-     * @return false
-     */
-    override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
-        val count = adapter.selectedItemCount
-        actionMode.title = getString(R.string.label_selected, count)
-
-        // Show edit button only when one item is selected
-        val editItem = actionMode.menu.findItem(R.id.action_edit)
-        editItem.isVisible = count == 1
-        return true
-    }
-
-    /**
-     * Called when action mode destroyed.
-     *
-     * @param mode ActionMode object.
-     */
-    override fun onDestroyActionMode(mode: ActionMode?) {
-        // Reset adapter to single selection
-        adapter.mode = FlexibleAdapter.MODE_IDLE
-        adapter.clearSelection()
-        actionMode = null
-    }
-
-    /**
-     * Called when item in list is clicked.
-     *
-     * @param position position of clicked item.
-     */
-    override fun onItemClick(position: Int): Boolean {
-        // Check if action mode is initialized and selected item exist.
-        if (actionMode != null && position != RecyclerView.NO_POSITION) {
-            toggleSelection(position)
-            return true
-        } else {
-            return false
-        }
-    }
-
-    /**
-     * Called when item long clicked
-     *
-     * @param position position of clicked item.
-     */
-    override fun onItemLongClick(position: Int) {
-        // Check if action mode is initialized.
-        if (actionMode == null) {
-            // Initialize action mode
-            actionMode = startSupportActionMode(this)
-        }
-
-        // Set item as selected
-        toggleSelection(position)
-    }
-
-    /**
-     * Toggle the selection state of an item.
-     * If the item was the last one in the selection and is unselected, the ActionMode is finished.
-     */
-    private fun toggleSelection(position: Int) {
-        //Mark the position selected
-        adapter.toggleSelection(position)
-
-        if (adapter.selectedItemCount == 0) {
-            actionMode?.finish()
-        } else {
-            actionMode?.invalidate()
-        }
-    }
-
-    /**
-     * Called when an item is released from a drag.
-     */
-    fun onItemReleased() {
-        val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
-        presenter.reorderCategories(categories)
-    }
-
-    /**
-     * Called when the undo action is clicked in the snackbar.
-     */
-    override fun onUndoConfirmed(action: Int) {
-        adapter.restoreDeletedItems()
-    }
-
-    /**
-     * Called when the time to restore the items expires.
-     */
-    override fun onDeleteConfirmed(action: Int) {
-        presenter.deleteCategories(adapter.deletedItems.map { it.category })
-    }
-
-}

+ 29 - 12
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt

@@ -3,31 +3,48 @@ package eu.kanade.tachiyomi.ui.category
 import eu.davidea.flexibleadapter.FlexibleAdapter
 
 /**
- * Adapter of CategoryHolder.
- * Connection between Activity and Holder
- * Holder updates should be called from here.
+ * Custom adapter for categories.
  *
- * @param activity activity that created adapter
- * @constructor Creates a CategoryAdapter object
+ * @param controller The containing controller.
  */
-class CategoryAdapter(private val activity: CategoryActivity) :
-        FlexibleAdapter<CategoryItem>(null, activity, true) {
+class CategoryAdapter(controller: CategoryController) :
+        FlexibleAdapter<CategoryItem>(null, controller, true) {
 
     /**
-     * Called when item is released.
+     * Listener called when an item of the list is released.
      */
-    fun onItemReleased() {
-        activity.onItemReleased()
-    }
+    val onItemReleaseListener: OnItemReleaseListener = controller
 
+    /**
+     * Clears the active selections from the list and the model.
+     */
     override fun clearSelection() {
         super.clearSelection()
-        (0..itemCount-1).forEach { getItem(it).isSelected = false }
+        (0 until itemCount).forEach { getItem(it).isSelected = false }
     }
 
+    /**
+     * Clears the active selections from the model.
+     */
+    fun clearModelSelection() {
+        selectedPositions.forEach { getItem(it).isSelected = false }
+    }
+
+    /**
+     * Toggles the selection of the given position.
+     *
+     * @param position The position to toggle.
+     */
     override fun toggleSelection(position: Int) {
         super.toggleSelection(position)
         getItem(position).isSelected = isSelected(position)
     }
 
+    interface OnItemReleaseListener {
+        /**
+         * Called when an item of the list is released.
+         */
+        fun onItemReleased(position: Int)
+    }
+
 }

+ 321 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt

@@ -0,0 +1,321 @@
+package eu.kanade.tachiyomi.ui.category
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.*
+import com.jakewharton.rxbinding.view.clicks
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.widget.UndoHelper
+import kotlinx.android.synthetic.main.categories_controller.view.*
+
+/**
+ * Controller to manage the categories for the users' library.
+ */
+class CategoryController : NucleusController<CategoryPresenter>(),
+        ActionMode.Callback,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        CategoryAdapter.OnItemReleaseListener,
+        CategoryCreateDialog.Listener,
+        CategoryRenameDialog.Listener,
+        UndoHelper.OnUndoListener {
+
+    /**
+     * Object used to show ActionMode toolbar.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Adapter containing category items.
+     */
+    private var adapter: CategoryAdapter? = null
+
+    /**
+     * Undo helper for deleting categories.
+     */
+    private var undoHelper: UndoHelper? = null
+
+    /**
+     * Creates the presenter for this controller. Not to be manually called.
+     */
+    override fun createPresenter() = CategoryPresenter()
+
+    /**
+     * Returns the toolbar title to show when this controller is attached.
+     */
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.action_edit_categories)
+    }
+
+    /**
+     * Returns the view of this controller.
+     *
+     * @param inflater The layout inflater to create the view from XML.
+     * @param container The parent view for this one.
+     */
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.categories_controller, container, false)
+    }
+
+    /**
+     * Called after view inflation. Used to initialize the view.
+     *
+     * @param view The view of this controller.
+     * @param savedViewState The saved state of the view.
+     */
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        with(view) {
+            adapter = CategoryAdapter(this@CategoryController)
+            recycler.layoutManager = LinearLayoutManager(context)
+            recycler.setHasFixedSize(true)
+            recycler.adapter = adapter
+            adapter?.isHandleDragEnabled = true
+
+            fab.clicks().subscribeUntilDestroy {
+                CategoryCreateDialog(this@CategoryController).showDialog(router, null)
+            }
+        }
+    }
+
+    /**
+     * Called when the view is being destroyed. Used to release references and remove callbacks.
+     *
+     * @param view The view of this controller.
+     */
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        undoHelper?.dismissNow() // confirm categories deletion if required
+        undoHelper = null
+        actionMode = null
+        adapter = null
+    }
+
+    /**
+     * Called from the presenter when the categories are updated.
+     *
+     * @param categories The new list of categories to display.
+     */
+    fun setCategories(categories: List<CategoryItem>) {
+        actionMode?.finish()
+        adapter?.updateDataSet(categories.toMutableList())
+        val selected = categories.filter { it.isSelected }
+        if (selected.isNotEmpty()) {
+            selected.forEach { onItemLongClick(categories.indexOf(it)) }
+        }
+    }
+
+    /**
+     * Called when action mode is first created. The menu supplied will be used to generate action
+     * buttons for the action mode.
+     *
+     * @param mode ActionMode being created.
+     * @param menu Menu used to populate action buttons.
+     * @return true if the action mode should be created, false if entering this mode should be
+     *              aborted.
+     */
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        // Inflate menu.
+        mode.menuInflater.inflate(R.menu.category_selection, menu)
+        // Enable adapter multi selection.
+        adapter?.mode = FlexibleAdapter.MODE_MULTI
+        return true
+    }
+
+    /**
+     * Called to refresh an action mode's action menu whenever it is invalidated.
+     *
+     * @param mode ActionMode being prepared.
+     * @param menu Menu used to populate action buttons.
+     * @return true if the menu or action mode was updated, false otherwise.
+     */
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val adapter = adapter ?: return false
+        val count = adapter.selectedItemCount
+        mode.title = resources?.getString(R.string.label_selected, count)
+
+        // Show edit button only when one item is selected
+        val editItem = mode.menu.findItem(R.id.action_edit)
+        editItem.isVisible = count == 1
+        return true
+    }
+
+    /**
+     * Called to report a user click on an action button.
+     *
+     * @param mode The current ActionMode.
+     * @param item The item that was clicked.
+     * @return true if this callback handled the event, false if the standard MenuItem invocation
+     *              should continue.
+     */
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        val adapter = adapter ?: return false
+
+        when (item.itemId) {
+            R.id.action_delete -> {
+                undoHelper = UndoHelper(adapter, this).apply {
+                    withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
+                        override fun onPreAction(): Boolean {
+                            adapter.clearModelSelection()
+                            return false
+                        }
+
+                        override fun onPostAction() {
+                            mode.finish()
+                        }
+                    })
+                    remove(adapter.selectedPositions, view!!,
+                            R.string.snack_categories_deleted, R.string.action_undo, 3000)
+                }
+            }
+            R.id.action_edit -> {
+                // Edit selected category
+                if (adapter.selectedItemCount == 1) {
+                    val position = adapter.selectedPositions.first()
+                    editCategory(adapter.getItem(position).category)
+                }
+            }
+            else -> return false
+        }
+        return true
+    }
+
+    /**
+     * Called when an action mode is about to be exited and destroyed.
+     *
+     * @param mode The current ActionMode being destroyed.
+     */
+    override fun onDestroyActionMode(mode: ActionMode) {
+        // Reset adapter to single selection
+        adapter?.mode = FlexibleAdapter.MODE_IDLE
+        adapter?.clearSelection()
+        actionMode = null
+    }
+
+    /**
+     * Called when an item in the list is clicked.
+     *
+     * @param position The position of the clicked item.
+     * @return true if this click should enable selection mode.
+     */
+    override fun onItemClick(position: Int): Boolean {
+        // Check if action mode is initialized and selected item exist.
+        if (actionMode != null && position != RecyclerView.NO_POSITION) {
+            toggleSelection(position)
+            return true
+        } else {
+            return false
+        }
+    }
+
+    /**
+     * Called when an item in the list is long clicked.
+     *
+     * @param position The position of the clicked item.
+     */
+    override fun onItemLongClick(position: Int) {
+        val activity = activity as? AppCompatActivity ?: return
+
+        // Check if action mode is initialized.
+        if (actionMode == null) {
+            // Initialize action mode
+            actionMode = activity.startSupportActionMode(this)
+        }
+
+        // Set item as selected
+        toggleSelection(position)
+    }
+
+    /**
+     * Toggle the selection state of an item.
+     * If the item was the last one in the selection and is unselected, the ActionMode is finished.
+     *
+     * @param position The position of the item to toggle.
+     */
+    private fun toggleSelection(position: Int) {
+        val adapter = adapter ?: return
+
+        //Mark the position selected
+        adapter.toggleSelection(position)
+
+        if (adapter.selectedItemCount == 0) {
+            actionMode?.finish()
+        } else {
+            actionMode?.invalidate()
+        }
+    }
+
+    /**
+     * 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 categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
+        presenter.reorderCategories(categories)
+    }
+
+    /**
+     * Called when the undo action is clicked in the snackbar.
+     *
+     * @param action The action performed.
+     */
+    override fun onUndoConfirmed(action: Int) {
+        adapter?.restoreDeletedItems()
+    }
+
+    /**
+     * Called when the time to restore the items expires.
+     *
+     * @param action The action performed.
+     */
+    override fun onDeleteConfirmed(action: Int) {
+        val adapter = adapter ?: return
+        presenter.deleteCategories(adapter.deletedItems.map { it.category })
+    }
+
+    /**
+     * Show a dialog to let the user change the category name.
+     *
+     * @param category The category to be edited.
+     */
+    private fun editCategory(category: Category) {
+        CategoryRenameDialog(this, category).showDialog(router)
+    }
+
+    /**
+     * Renames the given category with the given name.
+     *
+     * @param category The category to rename.
+     * @param name The new name of the category.
+     */
+    override fun renameCategory(category: Category, name: String) {
+        presenter.renameCategory(category, name)
+    }
+
+    /**
+     * Creates a new category with the given name.
+     *
+     * @param name The name of the new category.
+     */
+    override fun createCategory(name: String) {
+        presenter.createCategory(name)
+    }
+
+    /**
+     * Called from the presenter when a category with the given name already exists.
+     */
+    fun onCategoryExistsError() {
+        activity?.toast(R.string.error_category_exists)
+    }
+
+}

+ 47 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt

@@ -0,0 +1,47 @@
+package eu.kanade.tachiyomi.ui.category
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+/**
+ * Dialog to create a new category for the library.
+ */
+class CategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : CategoryCreateDialog.Listener {
+
+    /**
+     * Name of the new category. Value updated with each input from the user.
+     */
+    private var currentName = ""
+
+    constructor(target: T) : this() {
+        targetController = target
+    }
+
+    /**
+     * Called when creating the dialog for this controller.
+     *
+     * @param savedViewState The saved state of this dialog.
+     * @return a new dialog instance.
+     */
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_add_category)
+                .negativeText(android.R.string.cancel)
+                .alwaysCallInputCallback()
+                .input(resources?.getString(R.string.name), currentName, false, { _, input ->
+                    currentName = input.toString()
+                })
+                .onPositive { _, _ -> (targetController as? Listener)?.createCategory(currentName) }
+                .build()
+    }
+
+    interface Listener {
+        fun createCategory(name: String)
+    }
+
+}

+ 13 - 12
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt

@@ -10,14 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Category
 import kotlinx.android.synthetic.main.item_edit_categories.view.*
 
 /**
- * Holder that contains category item.
- * Uses R.layout.item_edit_categories.
- * UI related actions should be called from here.
+ * Holder used to display category items.
  *
- * @param view view of category item.
- * @param adapter adapter belonging to holder.
- *
- * @constructor Create CategoryHolder object
+ * @param view The view used by category items.
+ * @param adapter The adapter containing this holder.
  */
 class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
 
@@ -32,9 +28,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
     }
 
     /**
-     * Update category item values.
+     * Binds this holder with the given category.
      *
-     * @param category category of item.
+     * @param category The category to bind.
      */
     fun bind(category: Category) {
         // Set capitalized title.
@@ -47,9 +43,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
     }
 
     /**
-     * Returns circle letter image
+     * Returns circle letter image.
      *
-     * @param text first letter of string
+     * @param text The first letter of string.
      */
     private fun getRound(text: String): TextDrawable {
         val size = Math.min(itemView.image.width, itemView.image.height)
@@ -63,9 +59,14 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
                 .buildRound(text, ColorGenerator.MATERIAL.getColor(text))
     }
 
+    /**
+     * Called when an item is released.
+     *
+     * @param position The position of the released item.
+     */
     override fun onItemReleased(position: Int) {
         super.onItemReleased(position)
-        adapter.onItemReleased()
+        adapter.onItemReleaseListener.onItemReleased(position)
     }
 
 }

+ 36 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt

@@ -8,29 +8,62 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.util.inflate
 
+/**
+ * Category item for a recycler view.
+ */
 class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
 
+    /**
+     * Whether this item is currently selected.
+     */
     var isSelected = false
 
+    /**
+     * Returns the layout resource for this item.
+     */
     override fun getLayoutRes(): Int {
         return R.layout.item_edit_categories
     }
 
-    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
+    /**
+     * Returns a new view holder for this item.
+     *
+     * @param adapter The adapter of this item.
+     * @param inflater The layout inflater for XML inflation.
+     * @param parent The container view.
+     */
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
                                   parent: ViewGroup): CategoryHolder {
+
         return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
     }
 
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CategoryHolder,
-                                position: Int, payloads: List<Any?>?) {
+    /**
+     * 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<*>,
+                                holder: CategoryHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
         holder.bind(category)
     }
 
+    /**
+     * 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 CategoryItem) {
             return category.id == other.category.id
         }

+ 34 - 26
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt

@@ -1,31 +1,31 @@
 package eu.kanade.tachiyomi.ui.category
 
 import android.os.Bundle
-import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.util.toast
+import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
-import uy.kohesive.injekt.injectLazy
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
 /**
- * Presenter of CategoryActivity.
- * Contains information and data for activity.
- * Observable updates should be called from here.
+ * Presenter of [CategoryController]. Used to manage the categories of the library.
  */
-class CategoryPresenter : BasePresenter<CategoryActivity>() {
-
-    /**
-     * Used to connect to database.
-     */
-    private val db: DatabaseHelper by injectLazy()
+class CategoryPresenter(
+        private val db: DatabaseHelper = Injekt.get()
+) : BasePresenter<CategoryController>() {
 
     /**
      * List containing categories.
      */
     private var categories: List<Category> = emptyList()
 
+    /**
+     * Called when the presenter is created.
+     *
+     * @param savedState The saved state of this presenter.
+     */
     override fun onCreate(savedState: Bundle?) {
         super.onCreate(savedState)
 
@@ -33,18 +33,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
                 .doOnNext { categories = it }
                 .map { it.map(::CategoryItem) }
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(CategoryActivity::setCategories)
+                .subscribeLatestCache(CategoryController::setCategories)
     }
 
     /**
-     * Create category and add it to database
+     * Creates and adds a new category to the database.
      *
-     * @param name name of category
+     * @param name The name of the category to create.
      */
     fun createCategory(name: String) {
         // Do not allow duplicate categories.
-        if (categories.any { it.name.equals(name, true) }) {
-            context.toast(R.string.error_category_exists)
+        if (categoryExists(name)) {
+            Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
             return
         }
 
@@ -59,18 +59,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
     }
 
     /**
-     * Delete category from database
+     * Deletes the given categories from the database.
      *
-     * @param categories list of categories
+     * @param categories The list of categories to delete.
      */
     fun deleteCategories(categories: List<Category>) {
         db.deleteCategories(categories).asRxObservable().subscribe()
     }
 
     /**
-     * Reorder categories in database
+     * Reorders the given categories in the database.
      *
-     * @param categories list of categories
+     * @param categories The list of categories to reorder.
      */
     fun reorderCategories(categories: List<Category>) {
         categories.forEachIndexed { i, category ->
@@ -81,19 +81,27 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
     }
 
     /**
-     * Rename a category
+     * Renames a category.
      *
-     * @param category category that gets renamed
-     * @param name new name of category
+     * @param category The category to rename.
+     * @param name The new name of the category.
      */
     fun renameCategory(category: Category, name: String) {
         // Do not allow duplicate categories.
-        if (categories.any { it.name.equals(name, true) }) {
-            context.toast(R.string.error_category_exists)
+        if (categoryExists(name)) {
+            Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
             return
         }
 
         category.name = name
         db.insertCategory(category).asRxObservable().subscribe()
     }
+
+    /**
+     * Returns true if a category with the given name already exists.
+     */
+    fun categoryExists(name: String): Boolean {
+        return categories.any { it.name.equals(name, true) }
+    }
+
 }

+ 86 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt

@@ -0,0 +1,86 @@
+package eu.kanade.tachiyomi.ui.category
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+/**
+ * Dialog to rename an existing category of the library.
+ */
+class CategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : CategoryRenameDialog.Listener {
+
+    private var category: Category? = null
+
+    /**
+     * Name of the new category. Value updated with each input from the user.
+     */
+    private var currentName = ""
+
+    constructor(target: T, category: Category) : this() {
+        targetController = target
+        this.category = category
+        currentName = category.name
+    }
+
+    /**
+     * Called when creating the dialog for this controller.
+     *
+     * @param savedViewState The saved state of this dialog.
+     * @return a new dialog instance.
+     */
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_rename_category)
+                .negativeText(android.R.string.cancel)
+                .alwaysCallInputCallback()
+                .input(resources!!.getString(R.string.name), currentName, false, { _, input ->
+                    currentName = input.toString()
+                })
+                .onPositive { _, _ -> onPositive() }
+                .build()
+    }
+
+    /**
+     * Called to save this Controller's state in the event that its host Activity is destroyed.
+     *
+     * @param outState The Bundle into which data should be saved
+     */
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putSerializable(CATEGORY_KEY, category)
+        super.onSaveInstanceState(outState)
+    }
+
+    /**
+     * Restores data that was saved in the [onSaveInstanceState] method.
+     *
+     * @param savedInstanceState The bundle that has data to be restored
+     */
+    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+        super.onRestoreInstanceState(savedInstanceState)
+        category = savedInstanceState.getSerializable(CATEGORY_KEY) as? Category
+    }
+
+    /**
+     * Called when the positive button of the dialog is clicked.
+     */
+    private fun onPositive() {
+        val target = targetController as? Listener ?: return
+        val category = category ?: return
+
+        target.renameCategory(category, currentName)
+    }
+
+    interface Listener {
+        fun renameCategory(category: Category, name: String)
+    }
+
+    private companion object {
+        const val CATEGORY_KEY = "CategoryRenameDialog.category"
+    }
+
+}

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt

@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 import eu.kanade.tachiyomi.util.plusAssign
-import kotlinx.android.synthetic.main.activity_main.*
 import kotlinx.android.synthetic.main.fragment_download_queue.*
 import kotlinx.android.synthetic.main.toolbar.*
 import nucleus.factory.RequiresPresenter
@@ -242,6 +241,6 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
     }
 
     fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
-        if (show) empty_view.show(drawable, textResource) else empty_view.hide()
+//        if (show) empty_view.show(drawable, textResource) else empty_view.hide()
     }
 }

+ 33 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt

@@ -0,0 +1,33 @@
+package eu.kanade.tachiyomi.ui.latest_updates
+
+import android.support.v4.widget.DrawerLayout
+import android.view.Menu
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
+
+/**
+ * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
+ */
+class LatestUpdatesController : CatalogueController() {
+
+    override fun createPresenter(): CataloguePresenter {
+        return LatestUpdatesPresenter()
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        super.onPrepareOptionsMenu(menu)
+        menu.findItem(R.id.action_search).isVisible = false
+        menu.findItem(R.id.action_set_filter).isVisible = false
+    }
+
+    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
+        return null
+    }
+
+    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
+
+    }
+
+}

+ 0 - 29
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt

@@ -1,29 +0,0 @@
-package eu.kanade.tachiyomi.ui.latest_updates
-
-import android.view.Menu
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
-import nucleus.factory.RequiresPresenter
-
-/**
- * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
- */
-@RequiresPresenter(LatestUpdatesPresenter::class)
-class LatestUpdatesFragment : CatalogueFragment() {
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        super.onPrepareOptionsMenu(menu)
-        menu.findItem(R.id.action_search).isVisible = false
-        menu.findItem(R.id.action_set_filter).isVisible = false
-
-    }
-
-    companion object {
-
-        fun newInstance(): LatestUpdatesFragment {
-            return LatestUpdatesFragment()
-        }
-
-    }
-
-}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt

@@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
 import eu.kanade.tachiyomi.ui.catalogue.Pager
 
 /**
- * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
+ * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
  */
 class LatestUpdatesPresenter : CataloguePresenter() {
 

+ 48 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt

@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
+        DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
+
+    private var mangas = emptyList<Manga>()
+
+    private var categories = emptyList<Category>()
+
+    private var preselected = emptyArray<Int>()
+
+    constructor(target: T, mangas: List<Manga>, categories: List<Category>,
+                preselected: Array<Int>) : this() {
+
+        this.mangas = mangas
+        this.categories = categories
+        this.preselected = preselected
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_move_category)
+                .items(categories.map { it.name })
+                .itemsCallbackMultiChoice(preselected) { dialog, _, _ ->
+                    val newCategories = dialog.selectedIndices?.map { categories[it] }.orEmpty()
+                    (targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
+                    true
+                }
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .build()
+    }
+
+    interface Listener {
+        fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>)
+    }
+
+}

+ 43 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt

@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.widget.DialogCheckboxView
+
+class DeleteLibraryMangasDialog<T>(bundle: Bundle? = null) :
+        DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
+
+    private var mangas = emptyList<Manga>()
+
+    constructor(target: T, mangas: List<Manga>) : this() {
+        this.mangas = mangas
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val view = DialogCheckboxView(activity!!).apply {
+            setDescription(R.string.confirm_delete_manga)
+            setOptionDescription(R.string.also_delete_chapters)
+        }
+
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.action_remove)
+                .customView(view, true)
+                .positiveText(android.R.string.yes)
+                .negativeText(android.R.string.no)
+                .onPositive { _, _ ->
+                    val deleteChapters = view.isChecked()
+                    (targetController as? Listener)?.deleteMangasFromLibrary(mangas, deleteChapters)
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean)
+    }
+}

+ 87 - 87
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt

@@ -1,88 +1,88 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
-
-/**
- * This adapter stores the categories from the library, used with a ViewPager.
- *
- * @constructor creates an instance of the adapter.
- */
-class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() {
-
-    /**
-     * The categories to bind in the adapter.
-     */
-    var categories: List<Category> = emptyList()
-        // This setter helps to not refresh the adapter if the reference to the list doesn't change.
-        set(value) {
-            if (field !== value) {
-                field = value
-                notifyDataSetChanged()
-            }
-        }
-
-    /**
-     * Creates a new view for this adapter.
-     *
-     * @return a new view.
-     */
-    override fun createView(container: ViewGroup): View {
-        val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView
-        view.onCreate(fragment)
-        return view
-    }
-
-    /**
-     * Binds a view with a position.
-     *
-     * @param view the view to bind.
-     * @param position the position in the adapter.
-     */
-    override fun bindView(view: View, position: Int) {
-        (view as LibraryCategoryView).onBind(categories[position])
-    }
-
-    /**
-     * Recycles a view.
-     *
-     * @param view the view to recycle.
-     * @param position the position in the adapter.
-     */
-    override fun recycleView(view: View, position: Int) {
-        (view as LibraryCategoryView).onRecycle()
-    }
-
-    /**
-     * Returns the number of categories.
-     *
-     * @return the number of categories or 0 if the list is null.
-     */
-    override fun getCount(): Int {
-        return categories.size
-    }
-
-    /**
-     * Returns the title to display for a category.
-     *
-     * @param position the position of the element.
-     * @return the title to display.
-     */
-    override fun getPageTitle(position: Int): CharSequence {
-        return categories[position].name
-    }
-
-    /**
-     * Returns the position of the view.
-     */
-    override fun getItemPosition(obj: Any?): Int {
-        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
-        val index = categories.indexOfFirst { it.id == view.category.id }
-        return if (index == -1) POSITION_NONE else index
-    }
-
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
+
+/**
+ * This adapter stores the categories from the library, used with a ViewPager.
+ *
+ * @constructor creates an instance of the adapter.
+ */
+class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
+
+    /**
+     * The categories to bind in the adapter.
+     */
+    var categories: List<Category> = emptyList()
+        // This setter helps to not refresh the adapter if the reference to the list doesn't change.
+        set(value) {
+            if (field !== value) {
+                field = value
+                notifyDataSetChanged()
+            }
+        }
+
+    /**
+     * Creates a new view for this adapter.
+     *
+     * @return a new view.
+     */
+    override fun createView(container: ViewGroup): View {
+        val view = container.inflate(R.layout.item_library_category2) as LibraryCategoryView
+        view.onCreate(controller)
+        return view
+    }
+
+    /**
+     * Binds a view with a position.
+     *
+     * @param view the view to bind.
+     * @param position the position in the adapter.
+     */
+    override fun bindView(view: View, position: Int) {
+        (view as LibraryCategoryView).onBind(categories[position])
+    }
+
+    /**
+     * Recycles a view.
+     *
+     * @param view the view to recycle.
+     * @param position the position in the adapter.
+     */
+    override fun recycleView(view: View, position: Int) {
+        (view as LibraryCategoryView).onRecycle()
+    }
+
+    /**
+     * Returns the number of categories.
+     *
+     * @return the number of categories or 0 if the list is null.
+     */
+    override fun getCount(): Int {
+        return categories.size
+    }
+
+    /**
+     * Returns the title to display for a category.
+     *
+     * @param position the position of the element.
+     * @return the title to display.
+     */
+    override fun getPageTitle(position: Int): CharSequence {
+        return categories[position].name
+    }
+
+    /**
+     * Returns the position of the view.
+     */
+    override fun getItemPosition(obj: Any?): Int {
+        val view = obj as? LibraryCategoryView ?: return POSITION_NONE
+        val index = categories.indexOfFirst { it.id == view.category.id }
+        return if (index == -1) POSITION_NONE else index
+    }
+
 }

+ 44 - 122
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt

@@ -1,122 +1,44 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.Gravity
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.widget.FrameLayout
-import eu.davidea.flexibleadapter4.FlexibleAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
-import java.util.*
-
-/**
- * Adapter storing a list of manga in a certain category.
- *
- * @param fragment the fragment containing this adapter.
- */
-class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
-        FlexibleAdapter<LibraryHolder, Manga>() {
-
-    /**
-     * The list of manga in this category.
-     */
-    private var mangas: List<Manga> = emptyList()
-
-    init {
-        setHasStableIds(true)
-    }
-
-    /**
-     * Sets a list of manga in the adapter.
-     *
-     * @param list the list to set.
-     */
-    fun setItems(list: List<Manga>) {
-        mItems = list
-
-        // A copy of manga always unfiltered.
-        mangas = ArrayList(list)
-        updateDataSet(null)
-    }
-
-    /**
-     * Returns the identifier for a manga.
-     *
-     * @param position the position in the adapter.
-     * @return an identifier for the item.
-     */
-    override fun getItemId(position: Int): Long {
-        return mItems[position].id!!
-    }
-
-    /**
-     * Filters the list of manga applying [filterObject] for each element.
-     *
-     * @param param the filter. Not used.
-     */
-    override fun updateDataSet(param: String?) {
-        filterItems(mangas)
-        notifyDataSetChanged()
-    }
-
-    /**
-     * Filters a manga depending on a query.
-     *
-     * @param manga the manga to filter.
-     * @param query the query to apply.
-     * @return true if the manga should be included, false otherwise.
-     */
-    override fun filterObject(manga: Manga, query: String): Boolean = with(manga) {
-        title.toLowerCase().contains(query) ||
-                author != null && author!!.toLowerCase().contains(query)
-    }
-
-    /**
-     * 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): LibraryHolder {
-        // Depending on preferences, display a list or display a grid
-        if (parent is AutofitRecyclerView) {
-            val view = parent.inflate(R.layout.item_catalogue_grid).apply {
-                val coverHeight = parent.itemWidth / 3 * 4
-                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
-                gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
-            }
-            return LibraryGridHolder(view, this, fragment)
-        } else {
-            val view = parent.inflate(R.layout.item_catalogue_list)
-            return LibraryListHolder(view, this, fragment)
-        }
-    }
-
-    /**
-     * Binds a holder with a new position.
-     *
-     * @param holder the holder to bind.
-     * @param position the position to bind.
-     */
-    override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
-        val manga = getItem(position)
-
-        holder.onSetValues(manga)
-        // When user scrolls this bind the correct selection status
-        holder.itemView.isActivated = isSelected(position)
-    }
-
-    /**
-     * Returns the position in the adapter for the given manga.
-     *
-     * @param manga the manga to find.
-     */
-    fun indexOf(manga: Manga): Int {
-        return mangas.orEmpty().indexOfFirst { it.id == manga.id }
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Adapter storing a list of manga in a certain category.
+ *
+ * @param view the fragment containing this adapter.
+ */
+class LibraryCategoryAdapter(view: LibraryCategoryView) :
+        FlexibleAdapter<LibraryItem>(null, view, true) {
+
+    /**
+     * The list of manga in this category.
+     */
+    private var mangas: List<LibraryItem> = emptyList()
+
+    /**
+     * Sets a list of manga in the adapter.
+     *
+     * @param list the list to set.
+     */
+    fun setItems(list: List<LibraryItem>) {
+        // A copy of manga always unfiltered.
+        mangas = list.toList()
+
+        performFilter()
+    }
+
+    /**
+     * Returns the position in the adapter for the given manga.
+     *
+     * @param manga the manga to find.
+     */
+    fun indexOf(manga: Manga): Int {
+        return mangas.indexOfFirst { it.manga.id == manga.id }
+    }
+
+    fun performFilter() {
+        updateDataSet(mangas.filter { it.filter(searchText) })
+    }
+
+}

+ 248 - 266
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt

@@ -1,266 +1,248 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.content.Context
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import eu.davidea.flexibleadapter4.FlexibleAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.AutofitRecyclerView
-import kotlinx.android.synthetic.main.item_library_category.view.*
-import rx.Subscription
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Fragment containing the library manga for a certain category.
- * Uses R.layout.fragment_library_category.
- */
-class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
-: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener {
-
-    /**
-     * Preferences.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * The fragment containing this view.
-     */
-    private lateinit var fragment: LibraryFragment
-
-    /**
-     * Category for this view.
-     */
-    lateinit var category: Category
-        private set
-
-    /**
-     * Recycler view of the list of manga.
-     */
-    private lateinit var recycler: RecyclerView
-
-    /**
-     * Adapter to hold the manga in this category.
-     */
-    private lateinit var adapter: LibraryCategoryAdapter
-
-    /**
-     * Subscription for the library manga.
-     */
-    private var libraryMangaSubscription: Subscription? = null
-
-    /**
-     * Subscription of the library search.
-     */
-    private var searchSubscription: Subscription? = null
-
-    /**
-     * Subscription of the library selections.
-     */
-    private var selectionSubscription: Subscription? = null
-
-    fun onCreate(fragment: LibraryFragment) {
-        this.fragment = fragment
-
-        recycler = if (preferences.libraryAsList().getOrDefault()) {
-            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
-                layoutManager = LinearLayoutManager(context)
-            }
-        } else {
-            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
-                spanCount = fragment.mangaPerRow
-            }
-        }
-
-        adapter = LibraryCategoryAdapter(this)
-
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-        swipe_refresh.addView(recycler)
-
-        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
-                // Disable swipe refresh when view is not at the top
-                val firstPos = (recycler.layoutManager as LinearLayoutManager)
-                        .findFirstCompletelyVisibleItemPosition()
-                swipe_refresh.isEnabled = firstPos == 0
-            }
-        })
-
-        // Double the distance required to trigger sync
-        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
-        swipe_refresh.setOnRefreshListener {
-            if (!LibraryUpdateService.isRunning(context)) {
-                LibraryUpdateService.start(context, category)
-                context.toast(R.string.updating_category)
-            }
-            // It can be a very long operation, so we disable swipe refresh and show a toast.
-            swipe_refresh.isRefreshing = false
-        }
-    }
-
-    fun onBind(category: Category) {
-        this.category = category
-
-        val presenter = fragment.presenter
-
-        searchSubscription = presenter.searchSubject.subscribe { text ->
-            adapter.searchText = text
-            adapter.updateDataSet()
-        }
-
-        adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
-            FlexibleAdapter.MODE_MULTI
-        } else {
-            FlexibleAdapter.MODE_SINGLE
-        }
-
-        libraryMangaSubscription = presenter.libraryMangaSubject
-                .subscribe { onNextLibraryManga(it) }
-
-        selectionSubscription = presenter.selectionSubject
-                .subscribe { onSelectionChanged(it) }
-    }
-
-    fun onRecycle() {
-        adapter.setItems(emptyList())
-        adapter.clearSelection()
-    }
-
-    override fun onDetachedFromWindow() {
-        searchSubscription?.unsubscribe()
-        libraryMangaSubscription?.unsubscribe()
-        selectionSubscription?.unsubscribe()
-        super.onDetachedFromWindow()
-    }
-
-    /**
-     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
-     * adapter.
-     *
-     * @param event the event received.
-     */
-    fun onNextLibraryManga(event: LibraryMangaEvent) {
-        // Get the manga list for this category.
-        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
-
-        // Update the category with its manga.
-        adapter.setItems(mangaForCategory)
-
-        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
-            fragment.presenter.selectedMangas.forEach { manga ->
-                val position = adapter.indexOf(manga)
-                if (position != -1 && !adapter.isSelected(position)) {
-                    adapter.toggleSelection(position)
-                    (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
-                }
-            }
-        }
-    }
-
-    /**
-     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
-     * depending on the type of event received.
-     *
-     * @param event the selection event received.
-     */
-    private fun onSelectionChanged(event: LibrarySelectionEvent) {
-        when (event) {
-            is LibrarySelectionEvent.Selected -> {
-                if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
-                    adapter.mode = FlexibleAdapter.MODE_MULTI
-                }
-                findAndToggleSelection(event.manga)
-            }
-            is LibrarySelectionEvent.Unselected -> {
-                findAndToggleSelection(event.manga)
-                if (fragment.presenter.selectedMangas.isEmpty()) {
-                    adapter.mode = FlexibleAdapter.MODE_SINGLE
-                }
-            }
-            is LibrarySelectionEvent.Cleared -> {
-                adapter.mode = FlexibleAdapter.MODE_SINGLE
-                adapter.clearSelection()
-            }
-        }
-    }
-
-    /**
-     * Toggles the selection for the given manga and updates the view if needed.
-     *
-     * @param manga the manga to toggle.
-     */
-    private fun findAndToggleSelection(manga: Manga) {
-        val position = adapter.indexOf(manga)
-        if (position != -1) {
-            adapter.toggleSelection(position)
-            (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
-        }
-    }
-
-    /**
-     * Called when a manga is clicked.
-     *
-     * @param position the position of the element clicked.
-     * @return true if the item should be selected, false otherwise.
-     */
-    override fun onListItemClick(position: Int): Boolean {
-        // If the action mode is created and the position is valid, toggle the selection.
-        val item = adapter.getItem(position) ?: return false
-        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openManga(item)
-            return false
-        }
-    }
-
-    /**
-     * Called when a manga is long clicked.
-     *
-     * @param position the position of the element clicked.
-     */
-    override fun onListItemLongClick(position: Int) {
-        fragment.createActionModeIfNeeded()
-        toggleSelection(position)
-    }
-
-    /**
-     * Opens a manga.
-     *
-     * @param manga the manga to open.
-     */
-    private fun openManga(manga: Manga) {
-        // Notify the presenter a manga is being opened.
-        fragment.presenter.onOpenManga()
-
-        // Create a new activity with the manga.
-        val intent = MangaActivity.newIntent(context, manga)
-        fragment.startActivity(intent)
-    }
-
-
-    /**
-     * Tells the presenter to toggle the selection for the given position.
-     *
-     * @param position the position to toggle.
-     */
-    private fun toggleSelection(position: Int) {
-        val manga = adapter.getItem(position) ?: return
-
-        fragment.presenter.setSelection(manga, !adapter.isSelected(position))
-        fragment.invalidateActionMode()
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.content.Context
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.util.plusAssign
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.widget.AutofitRecyclerView
+import kotlinx.android.synthetic.main.item_library_category.view.*
+import rx.subscriptions.CompositeSubscription
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Fragment containing the library manga for a certain category.
+ * Uses R.layout.fragment_library_category.
+ */
+class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+        FrameLayout(context, attrs),
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener {
+
+    /**
+     * Preferences.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    /**
+     * The fragment containing this view.
+     */
+    private lateinit var controller: LibraryController
+
+    /**
+     * Category for this view.
+     */
+    lateinit var category: Category
+        private set
+
+    /**
+     * Recycler view of the list of manga.
+     */
+    private lateinit var recycler: RecyclerView
+
+    /**
+     * Adapter to hold the manga in this category.
+     */
+    private lateinit var adapter: LibraryCategoryAdapter
+
+    /**
+     * Subscriptions while the view is bound.
+     */
+    private var subscriptions = CompositeSubscription()
+
+    fun onCreate(controller: LibraryController) {
+        this.controller = controller
+
+        recycler = if (preferences.libraryAsList().getOrDefault()) {
+            (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
+                layoutManager = LinearLayoutManager(context)
+            }
+        } else {
+            (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
+                spanCount = controller.mangaPerRow
+            }
+        }
+
+        adapter = LibraryCategoryAdapter(this)
+
+        recycler.setHasFixedSize(true)
+        recycler.adapter = adapter
+        swipe_refresh.addView(recycler)
+
+        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
+                // Disable swipe refresh when view is not at the top
+                val firstPos = (recycler.layoutManager as LinearLayoutManager)
+                        .findFirstCompletelyVisibleItemPosition()
+                swipe_refresh.isEnabled = firstPos == 0
+            }
+        })
+
+        // Double the distance required to trigger sync
+        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
+        swipe_refresh.setOnRefreshListener {
+            if (!LibraryUpdateService.isRunning(context)) {
+                LibraryUpdateService.start(context, category)
+                context.toast(R.string.updating_category)
+            }
+            // It can be a very long operation, so we disable swipe refresh and show a toast.
+            swipe_refresh.isRefreshing = false
+        }
+    }
+
+    fun onBind(category: Category) {
+        this.category = category
+
+        adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
+            FlexibleAdapter.MODE_MULTI
+        } else {
+            FlexibleAdapter.MODE_SINGLE
+        }
+
+        subscriptions += controller.searchRelay
+                .doOnNext { adapter.searchText = it }
+                .skip(1)
+                .subscribe { adapter.performFilter() }
+
+        subscriptions += controller.libraryMangaRelay
+                .subscribe { onNextLibraryManga(it) }
+
+        subscriptions += controller.selectionRelay
+                .subscribe { onSelectionChanged(it) }
+    }
+
+    fun onRecycle() {
+        adapter.setItems(emptyList())
+        adapter.clearSelection()
+        subscriptions.clear()
+    }
+
+    override fun onDetachedFromWindow() {
+        subscriptions.clear()
+        super.onDetachedFromWindow()
+    }
+
+    /**
+     * Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
+     * adapter.
+     *
+     * @param event the event received.
+     */
+    fun onNextLibraryManga(event: LibraryMangaEvent) {
+        // Get the manga list for this category.
+        val mangaForCategory = event.getMangaForCategory(category).orEmpty()
+
+        // Update the category with its manga.
+        adapter.setItems(mangaForCategory)
+
+        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
+            controller.selectedMangas.forEach { manga ->
+                val position = adapter.indexOf(manga)
+                if (position != -1 && !adapter.isSelected(position)) {
+                    adapter.toggleSelection(position)
+                    (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
+                }
+            }
+        }
+    }
+
+    /**
+     * Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
+     * depending on the type of event received.
+     *
+     * @param event the selection event received.
+     */
+    private fun onSelectionChanged(event: LibrarySelectionEvent) {
+        when (event) {
+            is LibrarySelectionEvent.Selected -> {
+                if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
+                    adapter.mode = FlexibleAdapter.MODE_MULTI
+                }
+                findAndToggleSelection(event.manga)
+            }
+            is LibrarySelectionEvent.Unselected -> {
+                findAndToggleSelection(event.manga)
+                if (controller.selectedMangas.isEmpty()) {
+                    adapter.mode = FlexibleAdapter.MODE_SINGLE
+                }
+            }
+            is LibrarySelectionEvent.Cleared -> {
+                adapter.mode = FlexibleAdapter.MODE_SINGLE
+                adapter.clearSelection()
+            }
+        }
+    }
+
+    /**
+     * Toggles the selection for the given manga and updates the view if needed.
+     *
+     * @param manga the manga to toggle.
+     */
+    private fun findAndToggleSelection(manga: Manga) {
+        val position = adapter.indexOf(manga)
+        if (position != -1) {
+            adapter.toggleSelection(position)
+            (recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
+        }
+    }
+
+    /**
+     * Called when a manga is clicked.
+     *
+     * @param position the position of the element clicked.
+     * @return true if the item should be selected, false otherwise.
+     */
+    override fun onItemClick(position: Int): Boolean {
+        // If the action mode is created and the position is valid, toggle the selection.
+        val item = adapter.getItem(position) ?: return false
+        if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openManga(item.manga)
+            return false
+        }
+    }
+
+    /**
+     * Called when a manga is long clicked.
+     *
+     * @param position the position of the element clicked.
+     */
+    override fun onItemLongClick(position: Int) {
+        controller.createActionModeIfNeeded()
+        toggleSelection(position)
+    }
+
+    /**
+     * Opens a manga.
+     *
+     * @param manga the manga to open.
+     */
+    private fun openManga(manga: Manga) {
+        controller.openManga(manga)
+    }
+
+    /**
+     * Tells the presenter to toggle the selection for the given position.
+     *
+     * @param position the position to toggle.
+     */
+    private fun toggleSelection(position: Int) {
+        val item = adapter.getItem(position) ?: return
+
+        controller.setSelection(item.manga, !adapter.isSelected(position))
+        controller.invalidateActionMode()
+    }
+
+}

+ 510 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -0,0 +1,510 @@
+package eu.kanade.tachiyomi.ui.library
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Color
+import android.os.Bundle
+import android.support.design.widget.TabLayout
+import android.support.v4.graphics.drawable.DrawableCompat
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.f2prateek.rx.preferences.Preference
+import com.jakewharton.rxbinding.support.v4.view.pageSelections
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
+import eu.kanade.tachiyomi.ui.base.controller.TabbedController
+import eu.kanade.tachiyomi.ui.category.CategoryController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.util.toast
+import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.library_controller.view.*
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.IOException
+
+
+class LibraryController(
+        bundle: Bundle? = null,
+        private val preferences: PreferencesHelper = Injekt.get()
+) : NucleusController<LibraryPresenter>(bundle),
+        TabbedController,
+        SecondaryDrawerController,
+        ActionMode.Callback,
+        ChangeMangaCategoriesDialog.Listener,
+        DeleteLibraryMangasDialog.Listener {
+
+    /**
+     * Position of the active category.
+     */
+    var activeCategory: Int = preferences.lastUsedCategory().getOrDefault()
+        private set
+
+    /**
+     * Action mode for selections.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Library search query.
+     */
+    private var query = ""
+
+    /**
+     * Currently selected mangas.
+     */
+    val selectedMangas = mutableListOf<Manga>()
+
+    private var selectedCoverManga: Manga? = null
+
+    /**
+     * Relay to notify the UI of selection updates.
+     */
+    val selectionRelay: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
+
+    /**
+     * Relay to notify search query changes.
+     */
+    val searchRelay: BehaviorRelay<String> = BehaviorRelay.create()
+
+    /**
+     * Relay to notify the library's viewpager for updates.
+     */
+    val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
+
+    /**
+     * Number of manga per row in grid mode.
+     */
+    var mangaPerRow = 0
+        private set
+
+    /**
+     * TabLayout of the categories.
+     */
+    private val tabs: TabLayout?
+        get() = activity?.tabs
+
+    private val drawer: DrawerLayout?
+        get() = activity?.drawer
+
+    private var adapter: LibraryAdapter? = null
+
+    /**
+     * Navigation view containing filter/sort/display items.
+     */
+    private var navView: LibraryNavigationView? = null
+
+    /**
+     * Drawer listener to allow swipe only for closing the drawer.
+     */
+    private var drawerListener: DrawerLayout.DrawerListener? = null
+
+    init {
+        setHasOptionsMenu(true)
+    }
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_library)
+    }
+
+    override fun createPresenter(): LibraryPresenter {
+        return LibraryPresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.library_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        adapter = LibraryAdapter(this)
+        with(view) {
+            view_pager.adapter = adapter
+            view_pager.pageSelections().skip(1).subscribeUntilDestroy {
+                preferences.lastUsedCategory().set(it)
+                activeCategory = it
+            }
+
+            getColumnsPreferenceForCurrentOrientation().asObservable()
+                    .doOnNext { mangaPerRow = it }
+                    .skip(1)
+                    // Set again the adapter to recalculate the covers height
+                    .subscribeUntilDestroy { reattachAdapter() }
+
+            if (selectedMangas.isNotEmpty()) {
+                createActionModeIfNeeded()
+            }
+        }
+    }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeStarted(handler, type)
+        if (type.isEnter) {
+            activity?.tabs?.setupWithViewPager(view?.view_pager)
+        }
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        presenter.subscribeLibrary()
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+        actionMode = null
+    }
+
+    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
+        val view = drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
+        drawerListener = DrawerSwipeCloseListener(drawer, view).also {
+            drawer.addDrawerListener(it)
+        }
+        navView = view
+
+        navView?.post {
+            if (isAttached && drawer.isDrawerOpen(navView))
+                drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
+        }
+
+        navView?.onGroupClicked = { group ->
+            when (group) {
+                is LibraryNavigationView.FilterGroup -> onFilterChanged()
+                is LibraryNavigationView.SortGroup -> onSortChanged()
+                is LibraryNavigationView.DisplayGroup -> reattachAdapter()
+            }
+        }
+
+        return view
+    }
+
+    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
+        drawerListener?.let { drawer.removeDrawerListener(it) }
+        drawerListener = null
+        navView = null
+    }
+
+    fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
+        val view = view ?: return
+        val adapter = adapter ?: return
+
+        // Show empty view if needed
+        if (mangaMap.isNotEmpty()) {
+            view.empty_view.hide()
+        } else {
+            view.empty_view.show(R.drawable.ic_book_black_128dp, R.string.information_empty_library)
+        }
+
+        // Get the current active category.
+        val activeCat = if (adapter.categories.isNotEmpty())
+            view.view_pager.currentItem
+        else
+            activeCategory
+
+        // Set the categories
+        adapter.categories = categories
+
+        // Restore active category.
+        view.view_pager.setCurrentItem(activeCat, false)
+
+        tabs?.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
+
+        // Delay the scroll position to allow the view to be properly measured.
+        view.post {
+            if (isAttached) {
+                tabs?.setScrollPosition(view.view_pager.currentItem, 0f, true)
+            }
+        }
+
+        // Send the manga map to child fragments after the adapter is updated.
+        libraryMangaRelay.call(LibraryMangaEvent(mangaMap))
+    }
+
+    /**
+     * Returns a preference for the number of manga per row based on the current orientation.
+     *
+     * @return the preference.
+     */
+    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
+        return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
+            preferences.portraitColumns()
+        else
+            preferences.landscapeColumns()
+    }
+
+    /**
+     * Called when a filter is changed.
+     */
+    private fun onFilterChanged() {
+        presenter.requestFilterUpdate()
+        (activity as? AppCompatActivity)?.supportInvalidateOptionsMenu()
+    }
+
+    /**
+     * Called when the sorting mode is changed.
+     */
+    private fun onSortChanged() {
+        presenter.requestSortUpdate()
+    }
+
+    /**
+     * Reattaches the adapter to the view pager to recreate fragments
+     */
+    private fun reattachAdapter() {
+        val pager = view?.view_pager ?: return
+        val adapter = adapter ?: return
+
+        val position = pager.currentItem
+
+        adapter.recycle = false
+        pager.adapter = adapter
+        pager.currentItem = position
+        adapter.recycle = true
+    }
+
+    override fun configureTabs(tabs: TabLayout) {
+        with(tabs) {
+            tabGravity = TabLayout.GRAVITY_CENTER
+            tabMode = TabLayout.MODE_SCROLLABLE
+        }
+    }
+
+    /**
+     * Creates the action mode if it's not created already.
+     */
+    fun createActionModeIfNeeded() {
+        if (actionMode == null) {
+            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
+        }
+    }
+
+    /**
+     * Destroys the action mode.
+     */
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.library, menu)
+
+        val searchItem = menu.findItem(R.id.action_search)
+        val searchView = searchItem.actionView as SearchView
+
+        if (!query.isNullOrEmpty()) {
+            searchItem.expandActionView()
+            searchView.setQuery(query, true)
+            searchView.clearFocus()
+        }
+
+        // Mutate the filter icon because it needs to be tinted and the resource is shared.
+        menu.findItem(R.id.action_filter).icon.mutate()
+
+        searchView.queryTextChanges().subscribeUntilDestroy {
+            query = it.toString()
+            searchRelay.call(query)
+        }
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        val navView = navView ?: return
+
+        val filterItem = menu.findItem(R.id.action_filter)
+
+        // Tint icon if there's a filter active
+        val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
+        DrawableCompat.setTint(filterItem.icon, filterColor)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_filter -> {
+                navView?.let { drawer?.openDrawer(Gravity.END) }
+            }
+            R.id.action_update_library -> {
+                activity?.let { LibraryUpdateService.start(it) }
+            }
+            R.id.action_edit_categories -> {
+                router.pushController(RouterTransaction.with(CategoryController())
+                        .pushChangeHandler(FadeChangeHandler())
+                        .popChangeHandler(FadeChangeHandler()))
+            }
+            else -> return super.onOptionsItemSelected(item)
+        }
+
+        return true
+    }
+
+    /**
+     * Invalidates the action mode, forcing it to refresh its content.
+     */
+    fun invalidateActionMode() {
+        actionMode?.invalidate()
+    }
+
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.library_selection, menu)
+        return true
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = selectedMangas.size
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+            menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
+        }
+        return false
+    }
+
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_edit_cover -> {
+                changeSelectedCover()
+                destroyActionModeIfNeeded()
+            }
+            R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
+            R.id.action_delete -> showDeleteMangaDialog()
+            else -> return false
+        }
+        return true
+    }
+
+    override fun onDestroyActionMode(mode: ActionMode?) {
+        // Clear all the manga selections and notify child views.
+        selectedMangas.clear()
+        selectionRelay.call(LibrarySelectionEvent.Cleared())
+        actionMode = null
+    }
+
+    fun openManga(manga: Manga) {
+        // Notify the presenter a manga is being opened.
+        presenter.onOpenManga()
+
+        router.pushController(RouterTransaction.with(MangaController(manga))
+                .pushChangeHandler(FadeChangeHandler())
+                .popChangeHandler(FadeChangeHandler()))
+    }
+
+    /**
+     * Sets the selection for a given manga.
+     *
+     * @param manga the manga whose selection has changed.
+     * @param selected whether it's now selected or not.
+     */
+    fun setSelection(manga: Manga, selected: Boolean) {
+        if (selected) {
+            selectedMangas.add(manga)
+            selectionRelay.call(LibrarySelectionEvent.Selected(manga))
+        } else {
+            selectedMangas.remove(manga)
+            selectionRelay.call(LibrarySelectionEvent.Unselected(manga))
+        }
+    }
+
+    /**
+     * Move the selected manga to a list of categories.
+     */
+    private fun showChangeMangaCategoriesDialog() {
+        // Create a copy of selected manga
+        val mangas = selectedMangas.toList()
+
+        // Hide the default category because it has a different behavior than the ones from db.
+        val categories = presenter.categories.filter { it.id != 0 }
+
+        // Get indexes of the common categories to preselect.
+        val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
+                .map { categories.indexOf(it) }
+                .toTypedArray()
+
+        ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
+                .showDialog(router, null)
+    }
+
+    private fun showDeleteMangaDialog() {
+        DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router, null)
+    }
+
+    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
+        presenter.moveMangasToCategories(categories, mangas)
+        destroyActionModeIfNeeded()
+    }
+
+    override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
+        presenter.removeMangaFromLibrary(mangas, deleteChapters)
+        destroyActionModeIfNeeded()
+    }
+
+    /**
+     * Changes the cover for the selected manga.
+     *
+     * @param mangas a list of selected manga.
+     */
+    private fun changeSelectedCover() {
+        val manga = selectedMangas.firstOrNull() ?: return
+        selectedCoverManga = manga
+
+        if (manga.favorite) {
+            val intent = Intent(Intent.ACTION_GET_CONTENT)
+            intent.type = "image/*"
+            startActivityForResult(Intent.createChooser(intent,
+                    resources?.getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
+        } else {
+            activity?.toast(R.string.notification_first_add_to_library)
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_IMAGE_OPEN) {
+            if (data == null || resultCode != Activity.RESULT_OK) return
+            val activity = activity ?: return
+            val manga = selectedCoverManga ?: return
+
+            try {
+                // Get the file's input stream from the incoming Intent
+                activity.contentResolver.openInputStream(data.data).use {
+                    // Update cover to selected file, show error if something went wrong
+                    if (presenter.editCoverWithStream(it, manga)) {
+                        // TODO refresh cover
+                    } else {
+                        activity.toast(R.string.notification_cover_update_failed)
+                    }
+                }
+            } catch (error: IOException) {
+                activity.toast(R.string.notification_cover_update_failed)
+                Timber.e(error)
+            }
+            selectedCoverManga = null
+        }
+    }
+
+    private companion object {
+        /**
+         * Key to change the cover of a manga in [onActivityResult].
+         */
+        const val REQUEST_IMAGE_OPEN = 101
+    }
+
+}

+ 0 - 503
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt

@@ -1,503 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Activity
-import android.content.Intent
-import android.content.res.Configuration
-import android.graphics.Color
-import android.os.Bundle
-import android.support.design.widget.TabLayout
-import android.support.v4.graphics.drawable.DrawableCompat
-import android.support.v4.view.ViewPager
-import android.support.v4.widget.DrawerLayout
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.SearchView
-import android.view.*
-import com.afollestad.materialdialogs.MaterialDialog
-import com.f2prateek.rx.preferences.Preference
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.category.CategoryActivity
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.DialogCheckboxView
-import kotlinx.android.synthetic.main.activity_main.*
-import kotlinx.android.synthetic.main.fragment_library.*
-import nucleus.factory.RequiresPresenter
-import rx.Subscription
-import timber.log.Timber
-import uy.kohesive.injekt.injectLazy
-import java.io.IOException
-
-/**
- * Fragment that shows the manga from the library.
- * Uses R.layout.fragment_library.
- */
-@RequiresPresenter(LibraryPresenter::class)
-class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback {
-
-    /**
-     * Adapter containing the categories of the library.
-     */
-    lateinit var adapter: LibraryAdapter
-        private set
-
-    /**
-     * Preferences.
-     */
-    val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * TabLayout of the categories.
-     */
-    private val tabs: TabLayout
-        get() = (activity as MainActivity).tabs
-
-    /**
-     * Position of the active category.
-     */
-    private var activeCategory: Int = 0
-
-    /**
-     * Query of the search box.
-     */
-    private var query: String? = null
-
-    /**
-     * Action mode for manga selection.
-     */
-    private var actionMode: ActionMode? = null
-
-    /**
-     * Selected manga for editing its cover.
-     */
-    private var selectedCoverManga: Manga? = null
-
-    /**
-     * Number of manga per row in grid mode.
-     */
-    var mangaPerRow = 0
-        private set
-
-    /**
-     * Navigation view containing filter/sort/display items.
-     */
-    private lateinit var navView: LibraryNavigationView
-
-    /**
-     * Drawer listener to allow swipe only for closing the drawer.
-     */
-    private val drawerListener by lazy {
-        object : DrawerLayout.SimpleDrawerListener() {
-            override fun onDrawerClosed(drawerView: View) {
-                if (drawerView == navView) {
-                    activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
-                }
-            }
-
-            override fun onDrawerOpened(drawerView: View) {
-                if (drawerView == navView) {
-                    activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
-                }
-            }
-        }
-    }
-
-    /**
-     * Subscription for the number of manga per row.
-     */
-    private var numColumnsSubscription: Subscription? = null
-
-    companion object {
-        /**
-         * Key to change the cover of a manga in [onActivityResult].
-         */
-        const val REQUEST_IMAGE_OPEN = 101
-
-        /**
-         * Key to save and restore [query] from a [Bundle].
-         */
-        const val QUERY_KEY = "query_key"
-
-        /**
-         * Key to save and restore [activeCategory] from a [Bundle].
-         */
-        const val CATEGORY_KEY = "category_key"
-
-        /**
-         * Creates a new instance of this fragment.
-         *
-         * @return a new instance of [LibraryFragment].
-         */
-        fun newInstance(): LibraryFragment {
-            return LibraryFragment()
-        }
-    }
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        setHasOptionsMenu(true)
-    }
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
-        return inflater.inflate(R.layout.fragment_library, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedState: Bundle?) {
-        setToolbarTitle(getString(R.string.label_library))
-
-        adapter = LibraryAdapter(this)
-        view_pager.adapter = adapter
-        view_pager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
-            override fun onPageSelected(position: Int) {
-                preferences.lastUsedCategory().set(position)
-            }
-        })
-        tabs.setupWithViewPager(view_pager)
-
-        if (savedState != null) {
-            activeCategory = savedState.getInt(CATEGORY_KEY)
-            query = savedState.getString(QUERY_KEY)
-            presenter.searchSubject.call(query)
-            if (presenter.selectedMangas.isNotEmpty()) {
-                createActionModeIfNeeded()
-            }
-        } else {
-            activeCategory = preferences.lastUsedCategory().getOrDefault()
-        }
-
-        numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
-                .doOnNext { mangaPerRow = it }
-                .skip(1)
-                // Set again the adapter to recalculate the covers height
-                .subscribe { reattachAdapter() }
-
-
-        // Inflate and prepare drawer
-        navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
-        activity.drawer.addView(navView)
-        activity.drawer.addDrawerListener(drawerListener)
-
-        navView.post {
-            if (isAdded && !activity.drawer.isDrawerOpen(navView))
-                activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
-        }
-
-        navView.onGroupClicked = { group ->
-            when (group) {
-                is LibraryNavigationView.FilterGroup -> onFilterChanged()
-                is LibraryNavigationView.SortGroup -> onSortChanged()
-                is LibraryNavigationView.DisplayGroup -> reattachAdapter()
-            }
-        }
-    }
-
-    override fun onResume() {
-        super.onResume()
-        presenter.subscribeLibrary()
-    }
-
-    override fun onDestroyView() {
-        activity.drawer.removeDrawerListener(drawerListener)
-        activity.drawer.removeView(navView)
-        numColumnsSubscription?.unsubscribe()
-        tabs.setupWithViewPager(null)
-        tabs.visibility = View.GONE
-        super.onDestroyView()
-    }
-
-    override fun onSaveInstanceState(outState: Bundle) {
-        outState.putInt(CATEGORY_KEY, view_pager.currentItem)
-        outState.putString(QUERY_KEY, query)
-        super.onSaveInstanceState(outState)
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.library, menu)
-
-        val searchItem = menu.findItem(R.id.action_search)
-        val searchView = searchItem.actionView as SearchView
-
-        if (!query.isNullOrEmpty()) {
-            searchItem.expandActionView()
-            searchView.setQuery(query, true)
-            searchView.clearFocus()
-        }
-
-        // Mutate the filter icon because it needs to be tinted and the resource is shared.
-        menu.findItem(R.id.action_filter).icon.mutate()
-
-        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
-            override fun onQueryTextSubmit(query: String): Boolean {
-                onSearchTextChange(query)
-                return true
-            }
-
-            override fun onQueryTextChange(newText: String): Boolean {
-                onSearchTextChange(newText)
-                return true
-            }
-        })
-
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        val filterItem = menu.findItem(R.id.action_filter)
-
-        // Tint icon if there's a filter active
-        val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
-        DrawableCompat.setTint(filterItem.icon, filterColor)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_filter -> {
-                activity.drawer.openDrawer(Gravity.END)
-            }
-            R.id.action_update_library -> {
-                LibraryUpdateService.start(activity)
-            }
-            R.id.action_edit_categories -> {
-                val intent = CategoryActivity.newIntent(activity)
-                startActivity(intent)
-            }
-            else -> return super.onOptionsItemSelected(item)
-        }
-
-        return true
-    }
-
-    /**
-     * Called when a filter is changed.
-     */
-    private fun onFilterChanged() {
-        presenter.requestFilterUpdate()
-        activity.supportInvalidateOptionsMenu()
-    }
-
-    /**
-     * Called when the sorting mode is changed.
-     */
-    private fun onSortChanged() {
-        presenter.requestSortUpdate()
-    }
-
-    /**
-     * Reattaches the adapter to the view pager to recreate fragments
-     */
-    private fun reattachAdapter() {
-        val position = view_pager.currentItem
-        adapter.recycle = false
-        view_pager.adapter = adapter
-        view_pager.currentItem = position
-        adapter.recycle = true
-    }
-
-    /**
-     * Returns a preference for the number of manga per row based on the current orientation.
-     *
-     * @return the preference.
-     */
-    private fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
-        return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
-            preferences.portraitColumns()
-        else
-            preferences.landscapeColumns()
-    }
-
-    /**
-     * Updates the query.
-     *
-     * @param query the new value of the query.
-     */
-    private fun onSearchTextChange(query: String?) {
-        this.query = query
-
-        // Notify the subject the query has changed.
-        if (isResumed) {
-            presenter.searchSubject.call(query)
-        }
-    }
-
-    /**
-     * Called when the library is updated. It sets the new data and updates the view.
-     *
-     * @param categories the categories of the library.
-     * @param mangaMap a map containing the manga for each category.
-     */
-    fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<Manga>>) {
-        // Check if library is empty and update information accordingly.
-        (activity as MainActivity).updateEmptyView(mangaMap.isEmpty(),
-                R.string.information_empty_library, R.drawable.ic_book_black_128dp)
-
-        // Get the current active category.
-        val activeCat = if (adapter.categories.isNotEmpty()) view_pager.currentItem else activeCategory
-
-        // Set the categories
-        adapter.categories = categories
-        tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
-
-        // Restore active category.
-        view_pager.setCurrentItem(activeCat, false)
-        // Delay the scroll position to allow the view to be properly measured.
-        view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) }
-
-        // Send the manga map to child fragments after the adapter is updated.
-        presenter.libraryMangaSubject.call(LibraryMangaEvent(mangaMap))
-    }
-
-    /**
-     * Creates the action mode if it's not created already.
-     */
-    fun createActionModeIfNeeded() {
-        if (actionMode == null) {
-            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
-        }
-    }
-
-    /**
-     * Destroys the action mode.
-     */
-    fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    /**
-     * Invalidates the action mode, forcing it to refresh its content.
-     */
-    fun invalidateActionMode() {
-        actionMode?.invalidate()
-    }
-
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.library_selection, menu)
-        return true
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        val count = presenter.selectedMangas.size
-        if (count == 0) {
-            // Destroy action mode if there are no items selected.
-            destroyActionModeIfNeeded()
-        } else {
-            mode.title = getString(R.string.label_selected, count)
-            menu.findItem(R.id.action_edit_cover)?.isVisible = count == 1
-        }
-        return false
-    }
-
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_edit_cover -> {
-                changeSelectedCover(presenter.selectedMangas)
-                destroyActionModeIfNeeded()
-            }
-            R.id.action_move_to_category -> moveMangasToCategories(presenter.selectedMangas)
-            R.id.action_delete -> showDeleteMangaDialog()
-            else -> return false
-        }
-        return true
-    }
-
-    override fun onDestroyActionMode(mode: ActionMode) {
-        presenter.clearSelections()
-        actionMode = null
-    }
-
-    /**
-     * Changes the cover for the selected manga.
-     *
-     * @param mangas a list of selected manga.
-     */
-    private fun changeSelectedCover(mangas: List<Manga>) {
-        if (mangas.size == 1) {
-            selectedCoverManga = mangas[0]
-            if (selectedCoverManga?.favorite ?: false) {
-                val intent = Intent(Intent.ACTION_GET_CONTENT)
-                intent.type = "image/*"
-                startActivityForResult(Intent.createChooser(intent,
-                        getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
-            } else {
-                context.toast(R.string.notification_first_add_to_library)
-            }
-
-        }
-    }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMAGE_OPEN) {
-            selectedCoverManga?.let { manga ->
-
-                try {
-                    // Get the file's input stream from the incoming Intent
-                    context.contentResolver.openInputStream(data.data).use {
-                        // Update cover to selected file, show error if something went wrong
-                        if (presenter.editCoverWithStream(it, manga)) {
-                            // TODO refresh cover
-                        } else {
-                            context.toast(R.string.notification_cover_update_failed)
-                        }
-                    }
-                } catch (error: IOException) {
-                    context.toast(R.string.notification_cover_update_failed)
-                    Timber.e(error)
-                }
-            }
-
-        }
-    }
-
-    /**
-     * Move the selected manga to a list of categories.
-     *
-     * @param mangas the manga list to move.
-     */
-    private fun moveMangasToCategories(mangas: List<Manga>) {
-        // Hide the default category because it has a different behavior than the ones from db.
-        val categories = presenter.categories.filter { it.id != 0 }
-
-        // Get indexes of the common categories to preselect.
-        val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
-                .map { categories.indexOf(it) }
-                .toTypedArray()
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.action_move_category)
-                .items(categories.map { it.name })
-                .itemsCallbackMultiChoice(commonCategoriesIndexes) { dialog, positions, text ->
-                    val selectedCategories = positions.map { categories[it] }
-                    presenter.moveMangasToCategories(selectedCategories, mangas)
-                    destroyActionModeIfNeeded()
-                    true
-                }
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .show()
-    }
-
-    private fun showDeleteMangaDialog() {
-        val view = DialogCheckboxView(context).apply {
-            setDescription(R.string.confirm_delete_manga)
-            setOptionDescription(R.string.also_delete_chapters)
-        }
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.action_remove)
-                .customView(view, true)
-                .positiveText(android.R.string.yes)
-                .negativeText(android.R.string.no)
-                .onPositive { dialog, action ->
-                    val deleteChapters = view.isChecked()
-                    presenter.removeMangaFromLibrary(deleteChapters)
-                    destroyActionModeIfNeeded()
-                }
-                .show()
-    }
-
-}

+ 49 - 49
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt

@@ -1,49 +1,49 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
-import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_catalogue_grid" are available in this class.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-class LibraryGridHolder(private val view: View,
-                        private val adapter: LibraryCategoryAdapter,
-                        listener: FlexibleViewHolder.OnListItemClickListener)
-: LibraryHolder(view, adapter, listener) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param manga the manga to bind.
-     */
-    override fun onSetValues(manga: Manga) {
-        // Update the title of the manga.
-        view.title.text = manga.title
-
-        // Update the unread count and its visibility.
-        with(view.unread_text) {
-            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
-            text = manga.unread.toString()
-        }
-
-        // Update the cover.
-        Glide.clear(view.thumbnail)
-        Glide.with(view.context)
-                .load(manga)
-                .diskCacheStrategy(DiskCacheStrategy.RESULT)
-                .centerCrop()
-                .into(view.thumbnail)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
+
+/**
+ * Class used to hold the displayed data of a manga in the library, like the cover or the title.
+ * All the elements from the layout file "item_catalogue_grid" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to single tap and long tap events.
+ * @constructor creates a new library holder.
+ */
+class LibraryGridHolder(
+        private val view: View,
+        private val adapter: FlexibleAdapter<*>
+) : LibraryHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param manga the manga to bind.
+     */
+    override fun onSetValues(manga: Manga) {
+        // Update the title of the manga.
+        view.title.text = manga.title
+
+        // Update the unread count and its visibility.
+        with(view.unread_text) {
+            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
+            text = manga.unread.toString()
+        }
+
+        // Update the cover.
+        Glide.clear(view.thumbnail)
+        Glide.with(view.context)
+                .load(manga)
+                .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                .centerCrop()
+                .into(view.thumbnail)
+    }
+
+}

+ 28 - 27
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt

@@ -1,27 +1,28 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
-
-/**
- * Generic class used to hold the displayed data of a manga in the library.
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to the single tap and long tap events.
- */
-
-abstract class LibraryHolder(private val view: View,
-                             adapter: LibraryCategoryAdapter,
-                             listener: FlexibleViewHolder.OnListItemClickListener)
-: FlexibleViewHolder(view, adapter, listener) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param manga the manga to bind.
-     */
-    abstract fun onSetValues(manga: Manga)
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Generic class used to hold the displayed data of a manga in the library.
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to the single tap and long tap events.
+ */
+
+abstract class LibraryHolder(
+        view: View,
+        adapter: FlexibleAdapter<*>
+) : FlexibleViewHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param manga the manga to bind.
+     */
+    abstract fun onSetValues(manga: Manga)
+
+}

+ 70 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt

@@ -0,0 +1,70 @@
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFilterable
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.util.inflate
+import eu.kanade.tachiyomi.widget.AutofitRecyclerView
+import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
+
+class LibraryItem(val manga: Manga) : AbstractFlexibleItem<LibraryHolder>(), IFilterable {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.item_catalogue_grid
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
+                                  parent: ViewGroup): LibraryHolder {
+
+        return if (parent is AutofitRecyclerView) {
+            val view = parent.inflate(R.layout.item_catalogue_grid).apply {
+                val coverHeight = parent.itemWidth / 3 * 4
+                card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
+                gradient.layoutParams = FrameLayout.LayoutParams(
+                        MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
+            }
+            LibraryGridHolder(view, adapter)
+        } else {
+            val view = parent.inflate(R.layout.item_catalogue_list)
+            LibraryListHolder(view, adapter)
+        }
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: LibraryHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
+        holder.onSetValues(manga)
+    }
+
+    /**
+     * Filters a manga depending on a query.
+     *
+     * @param constraint the query to apply.
+     * @return true if the manga should be included, false otherwise.
+     */
+    override fun filter(constraint: String): Boolean {
+        return manga.title.contains(constraint, true) ||
+                (manga.author?.contains(constraint, true) ?: false)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other is LibraryItem) {
+            return manga.id == other.manga.id
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return manga.id!!.hashCode()
+    }
+}

+ 56 - 56
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt

@@ -1,57 +1,57 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.view.View
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
-import kotlinx.android.synthetic.main.item_catalogue_list.view.*
-
-/**
- * Class used to hold the displayed data of a manga in the library, like the cover or the title.
- * All the elements from the layout file "item_library_list" are available in this class.
- *
- * @param view the inflated view for this holder.
- * @param adapter the adapter handling this holder.
- * @param listener a listener to react to single tap and long tap events.
- * @constructor creates a new library holder.
- */
-
-class LibraryListHolder(private val view: View,
-                        private val adapter: LibraryCategoryAdapter,
-                        listener: FlexibleViewHolder.OnListItemClickListener)
-: LibraryHolder(view, adapter, listener) {
-
-    /**
-     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
-     * holder with the given manga.
-     *
-     * @param manga the manga to bind.
-     */
-    override fun onSetValues(manga: Manga) {
-        // Update the title of the manga.
-        itemView.title.text = manga.title
-
-        // Update the unread count and its visibility.
-        with(itemView.unread_text) {
-            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
-            text = manga.unread.toString()
-        }
-
-        // Create thumbnail onclick to simulate long click
-        itemView.thumbnail.setOnClickListener {
-            // Simulate long click on this view to enter selection mode
-            onLongClick(itemView)
-        }
-
-        // Update the cover.
-        Glide.clear(itemView.thumbnail)
-        Glide.with(itemView.context)
-                .load(manga)
-                .diskCacheStrategy(DiskCacheStrategy.RESULT)
-                .centerCrop()
-                .dontAnimate()
-                .into(itemView.thumbnail)
-    }
-
+package eu.kanade.tachiyomi.ui.library
+
+import android.view.View
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import kotlinx.android.synthetic.main.item_catalogue_list.view.*
+
+/**
+ * Class used to hold the displayed data of a manga in the library, like the cover or the title.
+ * All the elements from the layout file "item_library_list" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @param listener a listener to react to single tap and long tap events.
+ * @constructor creates a new library holder.
+ */
+
+class LibraryListHolder(
+        private val view: View,
+        private val adapter: FlexibleAdapter<*>
+) : LibraryHolder(view, adapter) {
+
+    /**
+     * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given manga.
+     *
+     * @param manga the manga to bind.
+     */
+    override fun onSetValues(manga: Manga) {
+        // Update the title of the manga.
+        itemView.title.text = manga.title
+
+        // Update the unread count and its visibility.
+        with(itemView.unread_text) {
+            visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
+            text = manga.unread.toString()
+        }
+
+        // Create thumbnail onclick to simulate long click
+        itemView.thumbnail.setOnClickListener {
+            // Simulate long click on this view to enter selection mode
+            onLongClick(itemView)
+        }
+
+        // Update the cover.
+        Glide.clear(itemView.thumbnail)
+        Glide.with(itemView.context)
+                .load(manga)
+                .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                .centerCrop()
+                .dontAnimate()
+                .into(itemView.thumbnail)
+    }
+
 }

+ 2 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt

@@ -1,11 +1,10 @@
 package eu.kanade.tachiyomi.ui.library
 
 import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
 
-class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) {
+class LibraryMangaEvent(val mangas: Map<Int, List<LibraryItem>>) {
 
-    fun getMangaForCategory(category: Category): List<Manga>? {
+    fun getMangaForCategory(category: Category): List<LibraryItem>? {
         return mangas[category.id]
     }
 }

+ 315 - 373
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -1,373 +1,315 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.os.Bundle
-import android.util.Pair
-import com.hippo.unifile.UniFile
-import com.jakewharton.rxrelay.BehaviorRelay
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.util.combineLatest
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.injectLazy
-import java.io.IOException
-import java.io.InputStream
-import java.util.*
-
-/**
- * Presenter of [LibraryFragment].
- */
-class LibraryPresenter : BasePresenter<LibraryFragment>() {
-
-    /**
-     * Database.
-     */
-    private val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Preferences.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * Cover cache.
-     */
-    private val coverCache: CoverCache by injectLazy()
-
-    /**
-     * Source manager.
-     */
-    private val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Download manager.
-     */
-    private val downloadManager: DownloadManager by injectLazy()
-
-    /**
-     * Categories of the library.
-     */
-    var categories: List<Category> = emptyList()
-
-    /**
-     * Currently selected manga.
-     */
-    val selectedMangas = mutableListOf<Manga>()
-
-    /**
-     * Search query of the library.
-     */
-    val searchSubject: BehaviorRelay<String> = BehaviorRelay.create()
-
-    /**
-     * Subject to notify the library's viewpager for updates.
-     */
-    val libraryMangaSubject: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
-
-    /**
-     * Subject to notify the UI of selection updates.
-     */
-    val selectionSubject: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
-
-    /**
-     * Relay used to apply the UI filters to the last emission of the library.
-     */
-    private val filterTriggerRelay = BehaviorRelay.create(Unit)
-
-    /**
-     * Relay used to apply the selected sorting method to the last emission of the library.
-     */
-    private val sortTriggerRelay = BehaviorRelay.create(Unit)
-
-    /**
-     * Library subscription.
-     */
-    private var librarySubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        subscribeLibrary()
-    }
-
-    /**
-     * Subscribes to library if needed.
-     */
-    fun subscribeLibrary() {
-        if (librarySubscription.isNullOrUnsubscribed()) {
-            librarySubscription = getLibraryObservable()
-                    .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
-                            { lib, tick -> Pair(lib.first, applyFilters(lib.second)) })
-                    .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
-                            { lib, tick -> Pair(lib.first, applySort(lib.second)) })
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribeLatestCache({ view, pair ->
-                        view.onNextLibraryUpdate(pair.first, pair.second)
-                    })
-        }
-    }
-
-    /**
-     * Applies library filters to the given map of manga.
-     *
-     * @param map the map to filter.
-     */
-    private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
-        // Cached list of downloaded manga directories given a source id.
-        val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
-
-        // Cached list of downloaded chapter directories for a manga.
-        val chapterDirectories = mutableMapOf<Long, Boolean>()
-
-        val filterDownloaded = preferences.filterDownloaded().getOrDefault()
-
-        val filterUnread = preferences.filterUnread().getOrDefault()
-
-        val filterFn: (Manga) -> Boolean = f@ { manga ->
-            // Filter out manga without source.
-            val source = sourceManager.get(manga.source) ?: return@f false
-
-            // Filter when there isn't unread chapters.
-            if (filterUnread && manga.unread == 0) {
-                return@f false
-            }
-
-            // Filter when the download directory doesn't exist or is null.
-            if (filterDownloaded) {
-                // Get the directories for the source of the manga.
-                val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
-                    val sourceDir = downloadManager.findSourceDir(source)
-                    sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
-                }
-
-                val mangaDirName = downloadManager.getMangaDirName(manga)
-                val mangaDir = dirsForSource[mangaDirName] ?: return@f false
-
-                val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
-                    mangaDir.listFiles()?.isNotEmpty() ?: false
-                }
-                if (!hasDirs) {
-                    return@f false
-                }
-            }
-            true
-        }
-
-        return map.mapValues { entry -> entry.value.filter(filterFn) }
-    }
-
-    /**
-     * Applies library sorting to the given map of manga.
-     *
-     * @param map the map to sort.
-     */
-    private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
-        val sortingMode = preferences.librarySortingMode().getOrDefault()
-
-        val lastReadManga by lazy {
-            var counter = 0
-            db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
-        }
-
-        val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
-            when (sortingMode) {
-                LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
-                LibrarySort.LAST_READ -> {
-                    // Get index of manga, set equal to list if size unknown.
-                    val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
-                    val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
-                    manga1LastRead.compareTo(manga2LastRead)
-                }
-                LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
-                LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
-                else -> throw Exception("Unknown sorting mode")
-            }
-        }
-
-        val comparator = if (preferences.librarySortingAscending().getOrDefault())
-            Comparator(sortFn)
-        else
-            Collections.reverseOrder(sortFn)
-
-        return map.mapValues { entry -> entry.value.sortedWith(comparator) }
-    }
-
-    /**
-     * Get the categories and all its manga from the database.
-     *
-     * @return an observable of the categories and its manga.
-     */
-    private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
-        return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
-                { dbCategories, libraryManga ->
-                    val categories = if (libraryManga.containsKey(0))
-                        arrayListOf(Category.createDefault()) + dbCategories
-                    else
-                        dbCategories
-
-                    this.categories = categories
-                    Pair(categories, libraryManga)
-                })
-    }
-
-    /**
-     * Get the categories from the database.
-     *
-     * @return an observable of the categories.
-     */
-    private fun getCategoriesObservable(): Observable<List<Category>> {
-        return db.getCategories().asRxObservable()
-    }
-
-    /**
-     * Get the manga grouped by categories.
-     *
-     * @return an observable containing a map with the category id as key and a list of manga as the
-     * value.
-     */
-    private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
-        return db.getLibraryMangas().asRxObservable()
-                .map { list -> list.groupBy { it.category } }
-    }
-
-    /**
-     * Requests the library to be filtered.
-     */
-    fun requestFilterUpdate() {
-        filterTriggerRelay.call(Unit)
-    }
-
-    /**
-     * Requests the library to be sorted.
-     */
-    fun requestSortUpdate() {
-        sortTriggerRelay.call(Unit)
-    }
-
-    /**
-     * Called when a manga is opened.
-     */
-    fun onOpenManga() {
-        // Avoid further db updates for the library when it's not needed
-        librarySubscription?.let { remove(it) }
-    }
-
-    /**
-     * Sets the selection for a given manga.
-     *
-     * @param manga the manga whose selection has changed.
-     * @param selected whether it's now selected or not.
-     */
-    fun setSelection(manga: Manga, selected: Boolean) {
-        if (selected) {
-            selectedMangas.add(manga)
-            selectionSubject.call(LibrarySelectionEvent.Selected(manga))
-        } else {
-            selectedMangas.remove(manga)
-            selectionSubject.call(LibrarySelectionEvent.Unselected(manga))
-        }
-    }
-
-    /**
-     * Clears all the manga selections and notifies the UI.
-     */
-    fun clearSelections() {
-        selectedMangas.clear()
-        selectionSubject.call(LibrarySelectionEvent.Cleared())
-    }
-
-    /**
-     * Returns the common categories for the given list of manga.
-     *
-     * @param mangas the list of manga.
-     */
-    fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
-        if (mangas.isEmpty()) return emptyList()
-        return mangas.toSet()
-                .map { db.getCategoriesForManga(it).executeAsBlocking() }
-                .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
-    }
-
-    /**
-     * Remove the selected manga from the library.
-     *
-     * @param deleteChapters whether to also delete downloaded chapters.
-     */
-    fun removeMangaFromLibrary(deleteChapters: Boolean) {
-        // Create a set of the list
-        val mangaToDelete = selectedMangas.distinctBy { it.id }
-        mangaToDelete.forEach { it.favorite = false }
-
-        Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
-                .onErrorResumeNext { Observable.empty() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-
-        Observable.fromCallable {
-            mangaToDelete.forEach { manga ->
-                coverCache.deleteFromCache(manga.thumbnail_url)
-                if (deleteChapters) {
-                    val source = sourceManager.get(manga.source) as? HttpSource
-                    if (source != null) {
-                        downloadManager.findMangaDir(source, manga)?.delete()
-                    }
-                }
-            }
-        }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Move the given list of manga to categories.
-     *
-     * @param categories the selected categories.
-     * @param mangas the list of manga to move.
-     */
-    fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
-        val mc = ArrayList<MangaCategory>()
-
-        for (manga in mangas) {
-            for (cat in categories) {
-                mc.add(MangaCategory.create(manga, cat))
-            }
-        }
-
-        db.setMangaCategories(mc, mangas)
-    }
-
-    /**
-     * Update cover with local file.
-     *
-     * @param inputStream the new cover.
-     * @param manga the manga edited.
-     * @return true if the cover is updated, false otherwise
-     */
-    @Throws(IOException::class)
-    fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
-        if (manga.source == LocalSource.ID) {
-            LocalSource.updateCover(context, manga, inputStream)
-            return true
-        }
-
-        if (manga.thumbnail_url != null && manga.favorite) {
-            coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
-            return true
-        }
-        return false
-    }
-
-}
+package eu.kanade.tachiyomi.ui.library
+
+import android.os.Bundle
+import android.util.Pair
+import com.hippo.unifile.UniFile
+import com.jakewharton.rxrelay.BehaviorRelay
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaCategory
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.combineLatest
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.IOException
+import java.io.InputStream
+import java.util.*
+
+/**
+ * Presenter of [LibraryController].
+ */
+class LibraryPresenter(
+        private val db: DatabaseHelper = Injekt.get(),
+        private val preferences: PreferencesHelper = Injekt.get(),
+        private val coverCache: CoverCache = Injekt.get(),
+        private val sourceManager: SourceManager = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get()
+) : BasePresenter<LibraryController>() {
+
+    private val context = preferences.context
+
+    /**
+     * Categories of the library.
+     */
+    var categories: List<Category> = emptyList()
+        private set
+
+    /**
+     * Relay used to apply the UI filters to the last emission of the library.
+     */
+    private val filterTriggerRelay = BehaviorRelay.create(Unit)
+
+    /**
+     * Relay used to apply the selected sorting method to the last emission of the library.
+     */
+    private val sortTriggerRelay = BehaviorRelay.create(Unit)
+
+    /**
+     * Library subscription.
+     */
+    private var librarySubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        subscribeLibrary()
+    }
+
+    /**
+     * Subscribes to library if needed.
+     */
+    fun subscribeLibrary() {
+        if (librarySubscription.isNullOrUnsubscribed()) {
+            librarySubscription = getLibraryObservable()
+                    .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
+                            { lib, _ -> Pair(lib.first, applyFilters(lib.second)) })
+                    .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
+                            { lib, _ -> Pair(lib.first, applySort(lib.second)) })
+                    .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) }
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribeLatestCache({ view, pair ->
+                        view.onNextLibraryUpdate(pair.first, pair.second)
+                    })
+        }
+    }
+
+    /**
+     * Applies library filters to the given map of manga.
+     *
+     * @param map the map to filter.
+     */
+    private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
+        // Cached list of downloaded manga directories given a source id.
+        val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
+
+        // Cached list of downloaded chapter directories for a manga.
+        val chapterDirectories = mutableMapOf<Long, Boolean>()
+
+        val filterDownloaded = preferences.filterDownloaded().getOrDefault()
+
+        val filterUnread = preferences.filterUnread().getOrDefault()
+
+        val filterFn: (Manga) -> Boolean = f@ { manga ->
+            // Filter out manga without source.
+            val source = sourceManager.get(manga.source) ?: return@f false
+
+            // Filter when there isn't unread chapters.
+            if (filterUnread && manga.unread == 0) {
+                return@f false
+            }
+
+            // Filter when the download directory doesn't exist or is null.
+            if (filterDownloaded) {
+                // Get the directories for the source of the manga.
+                val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
+                    val sourceDir = downloadManager.findSourceDir(source)
+                    sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
+                }
+
+                val mangaDirName = downloadManager.getMangaDirName(manga)
+                val mangaDir = dirsForSource[mangaDirName] ?: return@f false
+
+                val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
+                    mangaDir.listFiles()?.isNotEmpty() ?: false
+                }
+                if (!hasDirs) {
+                    return@f false
+                }
+            }
+            true
+        }
+
+        return map.mapValues { entry -> entry.value.filter(filterFn) }
+    }
+
+    /**
+     * Applies library sorting to the given map of manga.
+     *
+     * @param map the map to sort.
+     */
+    private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
+        val sortingMode = preferences.librarySortingMode().getOrDefault()
+
+        val lastReadManga by lazy {
+            var counter = 0
+            db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
+        }
+
+        val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
+            when (sortingMode) {
+                LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
+                LibrarySort.LAST_READ -> {
+                    // Get index of manga, set equal to list if size unknown.
+                    val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
+                    val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
+                    manga1LastRead.compareTo(manga2LastRead)
+                }
+                LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
+                LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
+                else -> throw Exception("Unknown sorting mode")
+            }
+        }
+
+        val comparator = if (preferences.librarySortingAscending().getOrDefault())
+            Comparator(sortFn)
+        else
+            Collections.reverseOrder(sortFn)
+
+        return map.mapValues { entry -> entry.value.sortedWith(comparator) }
+    }
+
+    /**
+     * Get the categories and all its manga from the database.
+     *
+     * @return an observable of the categories and its manga.
+     */
+    private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
+        return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
+                { dbCategories, libraryManga ->
+                    val categories = if (libraryManga.containsKey(0))
+                        arrayListOf(Category.createDefault()) + dbCategories
+                    else
+                        dbCategories
+
+                    this.categories = categories
+                    Pair(categories, libraryManga)
+                })
+    }
+
+    /**
+     * Get the categories from the database.
+     *
+     * @return an observable of the categories.
+     */
+    private fun getCategoriesObservable(): Observable<List<Category>> {
+        return db.getCategories().asRxObservable()
+    }
+
+    /**
+     * Get the manga grouped by categories.
+     *
+     * @return an observable containing a map with the category id as key and a list of manga as the
+     * value.
+     */
+    private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
+        return db.getLibraryMangas().asRxObservable()
+                .map { list -> list.groupBy { it.category } }
+    }
+
+    /**
+     * Requests the library to be filtered.
+     */
+    fun requestFilterUpdate() {
+        filterTriggerRelay.call(Unit)
+    }
+
+    /**
+     * Requests the library to be sorted.
+     */
+    fun requestSortUpdate() {
+        sortTriggerRelay.call(Unit)
+    }
+
+    /**
+     * Called when a manga is opened.
+     */
+    fun onOpenManga() {
+        // Avoid further db updates for the library when it's not needed
+        librarySubscription?.let { remove(it) }
+    }
+
+    /**
+     * Returns the common categories for the given list of manga.
+     *
+     * @param mangas the list of manga.
+     */
+    fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
+        if (mangas.isEmpty()) return emptyList()
+        return mangas.toSet()
+                .map { db.getCategoriesForManga(it).executeAsBlocking() }
+                .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
+    }
+
+    /**
+     * Remove the selected manga from the library.
+     *
+     * @param mangas the list of manga to delete.
+     * @param deleteChapters whether to also delete downloaded chapters.
+     */
+    fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
+        // Create a set of the list
+        val mangaToDelete = mangas.distinctBy { it.id }
+        mangaToDelete.forEach { it.favorite = false }
+
+        Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
+                .onErrorResumeNext { Observable.empty() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+
+        Observable.fromCallable {
+            mangaToDelete.forEach { manga ->
+                coverCache.deleteFromCache(manga.thumbnail_url)
+                if (deleteChapters) {
+                    val source = sourceManager.get(manga.source) as? HttpSource
+                    if (source != null) {
+                        downloadManager.findMangaDir(source, manga)?.delete()
+                    }
+                }
+            }
+        }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Move the given list of manga to categories.
+     *
+     * @param categories the selected categories.
+     * @param mangas the list of manga to move.
+     */
+    fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
+        val mc = ArrayList<MangaCategory>()
+
+        for (manga in mangas) {
+            for (cat in categories) {
+                mc.add(MangaCategory.create(manga, cat))
+            }
+        }
+
+        db.setMangaCategories(mc, mangas)
+    }
+
+    /**
+     * Update cover with local file.
+     *
+     * @param inputStream the new cover.
+     * @param manga the manga edited.
+     * @return true if the cover is updated, false otherwise
+     */
+    @Throws(IOException::class)
+    fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
+        if (manga.source == LocalSource.ID) {
+            LocalSource.updateCover(context, manga, inputStream)
+            return true
+        }
+
+        if (manga.thumbnail_url != null && manga.favorite) {
+            coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
+            return true
+        }
+        return false
+    }
+
+}

+ 247 - 160
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -1,160 +1,247 @@
-package eu.kanade.tachiyomi.ui.main
-
-import android.content.Intent
-import android.os.Bundle
-import android.support.v4.app.Fragment
-import android.support.v4.app.TaskStackBuilder
-import android.support.v4.view.GravityCompat
-import android.view.MenuItem
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
-import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
-import eu.kanade.tachiyomi.ui.download.DownloadActivity
-import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
-import eu.kanade.tachiyomi.ui.library.LibraryFragment
-import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
-import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
-import eu.kanade.tachiyomi.ui.setting.SettingsActivity
-import kotlinx.android.synthetic.main.activity_main.*
-import kotlinx.android.synthetic.main.toolbar.*
-import uy.kohesive.injekt.injectLazy
-
-class MainActivity : BaseActivity() {
-
-    val preferences: PreferencesHelper by injectLazy()
-
-    private val startScreenId by lazy {
-        when (preferences.startScreen()) {
-            1 -> R.id.nav_drawer_library
-            2 -> R.id.nav_drawer_recently_read
-            3 -> R.id.nav_drawer_recent_updates
-            else -> R.id.nav_drawer_library
-        }
-    }
-
-    override fun onCreate(savedState: Bundle?) {
-        setAppTheme()
-        super.onCreate(savedState)
-
-        // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
-        if (!isTaskRoot) {
-            finish()
-            return
-        }
-
-        // Inflate activity_main.xml.
-        setContentView(R.layout.activity_main)
-
-        // Handle Toolbar
-        setupToolbar(toolbar, backNavigation = false)
-        supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp)
-
-        // Set behavior of Navigation drawer
-        nav_view.setNavigationItemSelectedListener { item ->
-            // Make information view invisible
-            empty_view.hide()
-
-            val id = item.itemId
-
-            val oldFragment = supportFragmentManager.findFragmentById(R.id.frame_container)
-            if (oldFragment == null || oldFragment.tag.toInt() != id) {
-                when (id) {
-                    R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id)
-                    R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id)
-                    R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
-                    R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id)
-                    R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id)
-                    R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java))
-                    R.id.nav_drawer_settings -> {
-                        val intent = Intent(this, SettingsActivity::class.java)
-                        startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
-                    }
-                }
-            }
-            drawer.closeDrawer(GravityCompat.START)
-            true
-        }
-
-        if (savedState == null) {
-            // Set start screen
-            when (intent.action) {
-                SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
-                SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
-                SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
-                SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
-                else ->  setSelectedDrawerItem(startScreenId)
-            }
-
-            // Show changelog if needed
-            ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
-        }
-
-
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            android.R.id.home -> drawer.openDrawer(GravityCompat.START)
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    override fun onBackPressed() {
-        val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
-        if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
-            drawer.closeDrawers()
-        } else if (fragment != null && fragment.tag.toInt() != startScreenId) {
-            if (resumed) {
-                setSelectedDrawerItem(startScreenId)
-            }
-        } else {
-            super.onBackPressed()
-        }
-    }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) {
-            if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) {
-                // If database is cleared avoid undefined behavior by recreating the stack.
-                TaskStackBuilder.create(this)
-                        .addNextIntent(Intent(this, MainActivity::class.java))
-                        .startActivities()
-            } else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
-                // Delay activity recreation to avoid fragment leaks.
-                nav_view.post { recreate() }
-            } else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
-                nav_view.post { recreate() }
-            }
-        } else {
-            super.onActivityResult(requestCode, resultCode, data)
-        }
-    }
-
-    private fun setSelectedDrawerItem(itemId: Int, triggerAction: Boolean = true) {
-        nav_view.setCheckedItem(itemId)
-        if (triggerAction) {
-            nav_view.menu.performIdentifierAction(itemId, 0)
-        }
-    }
-
-    private fun setFragment(fragment: Fragment, itemId: Int) {
-        supportFragmentManager.beginTransaction()
-                .replace(R.id.frame_container, fragment, "$itemId")
-                .commit()
-    }
-
-    fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
-        if (show) empty_view.show(drawable, textResource) else empty_view.hide()
-    }
-
-    companion object {
-        private const val REQUEST_OPEN_SETTINGS = 200
-        // Shortcut actions
-        private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
-        private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
-        private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
-        private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
-    }
-}
+package eu.kanade.tachiyomi.ui.main
+
+import android.animation.ObjectAnimator
+import android.app.TaskStackBuilder
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.support.v4.view.GravityCompat
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.graphics.drawable.DrawerArrowDrawable
+import android.view.ViewGroup
+import com.bluelinelabs.conductor.*
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
+import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
+import eu.kanade.tachiyomi.ui.base.controller.TabbedController
+import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.download.DownloadActivity
+import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
+import eu.kanade.tachiyomi.ui.library.LibraryController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
+import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
+import eu.kanade.tachiyomi.ui.setting.SettingsActivity
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.toolbar.*
+import uy.kohesive.injekt.injectLazy
+
+
+class MainActivity : BaseActivity() {
+
+    private lateinit var router: Router
+
+    val preferences: PreferencesHelper by injectLazy()
+
+    private var drawerArrow: DrawerArrowDrawable? = null
+
+    private var secondaryDrawer: ViewGroup? = null
+
+    private val startScreenId by lazy {
+        when (preferences.startScreen()) {
+            1 -> R.id.nav_drawer_library
+            2 -> R.id.nav_drawer_recently_read
+            3 -> R.id.nav_drawer_recent_updates
+            else -> R.id.nav_drawer_library
+        }
+    }
+
+    private val tabAnimator by lazy { TabsAnimator(tabs) }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setAppTheme()
+        super.onCreate(savedInstanceState)
+
+        // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
+        if (!isTaskRoot) {
+            finish()
+            return
+        }
+
+        setContentView(R.layout.activity_main)
+
+        setSupportActionBar(toolbar)
+
+        drawerArrow = DrawerArrowDrawable(this)
+        drawerArrow?.color = Color.WHITE
+        toolbar.navigationIcon = drawerArrow
+
+        // Set behavior of Navigation drawer
+        nav_view.setNavigationItemSelectedListener { item ->
+            val id = item.itemId
+
+            val currentRoot = router.backstack.firstOrNull()
+            if (currentRoot?.tag()?.toIntOrNull() != id) {
+                when (id) {
+                    R.id.nav_drawer_library -> setRoot(LibraryController(), id)
+                    R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
+                    R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
+                    R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
+                    R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
+                    R.id.nav_drawer_downloads -> {
+                        startActivity(Intent(this, DownloadActivity::class.java))
+                    }
+                    R.id.nav_drawer_settings -> {
+                        val intent = Intent(this, SettingsActivity::class.java)
+                        startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
+                    }
+                }
+            }
+            drawer.closeDrawer(GravityCompat.START)
+            true
+        }
+
+        val container = findViewById(R.id.controller_container) as ViewGroup
+
+        router = Conductor.attachRouter(this, container, savedInstanceState)
+        if (!router.hasRootController()) {
+            // Set start screen
+            when (intent.action) {
+                SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
+                SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
+                SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
+                SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
+                SHORTCUT_MANGA -> router.setRoot(
+                        RouterTransaction.with(MangaController(intent.extras)))
+                else -> setSelectedDrawerItem(startScreenId)
+            }
+        }
+
+        toolbar.setNavigationOnClickListener {
+            if (router.backstackSize == 1) {
+                drawer.openDrawer(GravityCompat.START)
+            } else {
+                onBackPressed()
+            }
+        }
+
+        router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener {
+            override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean,
+                                         container: ViewGroup, handler: ControllerChangeHandler) {
+
+                syncActivityViewWithController(to, from)
+            }
+
+            override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean,
+                                           container: ViewGroup, handler: ControllerChangeHandler) {
+
+            }
+
+        })
+
+        syncActivityViewWithController(router.backstack.lastOrNull()?.controller())
+
+        // TODO changelog controller
+        if (savedInstanceState == null) {
+            // Show changelog if needed
+            ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        nav_view?.setNavigationItemSelectedListener(null)
+        toolbar?.setNavigationOnClickListener(null)
+    }
+
+    override fun onBackPressed() {
+        val backstackSize = router.backstackSize
+        if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
+            drawer.closeDrawers()
+        } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
+            setSelectedDrawerItem(startScreenId)
+        } else if (backstackSize == 1 || !router.handleBack()) {
+            super.onBackPressed()
+        }
+    }
+
+    private fun setSelectedDrawerItem(itemId: Int) {
+        if (!isFinishing) {
+            nav_view.setCheckedItem(itemId)
+            nav_view.menu.performIdentifierAction(itemId, 0)
+        }
+    }
+
+    private fun setRoot(controller: Controller, id: Int) {
+        router.setRoot(RouterTransaction.with(controller)
+                .popChangeHandler(FadeChangeHandler())
+                .pushChangeHandler(FadeChangeHandler())
+                .tag(id.toString()))
+    }
+
+    private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {
+        if (from is DialogController || to is DialogController) {
+            return
+        }
+
+        val showHamburger = router.backstackSize == 1
+        if (showHamburger) {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
+        } else {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+        }
+
+        ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start()
+
+        if (from is TabbedController) {
+            from.cleanupTabs(tabs)
+        }
+        if (to is TabbedController) {
+            to.configureTabs(tabs)
+            tabAnimator.expand()
+        } else {
+            tabAnimator.collapse()
+            tabs.setupWithViewPager(null)
+        }
+
+        if (from is SecondaryDrawerController) {
+            if (secondaryDrawer != null) {
+                from.cleanupSecondaryDrawer(drawer)
+                drawer.removeView(secondaryDrawer)
+                secondaryDrawer = null
+            }
+        }
+        if (to is SecondaryDrawerController) {
+            secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) }
+        }
+
+        if (to is NoToolbarElevationController) {
+            appbar.disableElevation()
+        } else {
+            appbar.enableElevation()
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) {
+            if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) {
+                // If database is cleared avoid undefined behavior by recreating the stack.
+                TaskStackBuilder.create(this)
+                        .addNextIntent(Intent(this, MainActivity::class.java))
+                        .startActivities()
+            } else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
+                // Delay activity recreation to avoid fragment leaks.
+                nav_view.post { recreate() }
+            } else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
+                nav_view.post { recreate() }
+            }
+        } else {
+            super.onActivityResult(requestCode, resultCode, data)
+        }
+    }
+
+    companion object {
+        private const val REQUEST_OPEN_SETTINGS = 200
+        // Shortcut actions
+        private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
+        private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
+        private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
+        private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
+        const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
+    }
+
+}

+ 74 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt

@@ -0,0 +1,74 @@
+package eu.kanade.tachiyomi.ui.main
+
+import android.support.design.widget.TabLayout
+import android.view.animation.Animation
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Transformation
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.visible
+
+class TabsAnimator(val tabs: TabLayout) {
+
+    private var height = 0
+
+    private val interpolator = DecelerateInterpolator()
+
+    private val duration = 300L
+
+    private val expandAnimation = object : Animation() {
+        override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
+            tabs.layoutParams.height = (height * interpolatedTime).toInt()
+            tabs.requestLayout()
+        }
+
+        override fun willChangeBounds(): Boolean {
+            return true
+        }
+    }
+
+    private val collapseAnimation = object : Animation() {
+        override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
+            if (interpolatedTime == 1f) {
+                tabs.gone()
+            } else {
+                tabs.layoutParams.height = (height * (1 - interpolatedTime)).toInt()
+                tabs.requestLayout()
+            }
+        }
+
+        override fun willChangeBounds(): Boolean {
+            return true
+        }
+    }
+
+    init {
+        collapseAnimation.duration = duration
+        collapseAnimation.interpolator = interpolator
+        expandAnimation.duration = duration
+        expandAnimation.interpolator = interpolator
+    }
+
+    fun expand() {
+        tabs.visible()
+        if (measure() && tabs.measuredHeight != height) {
+            tabs.startAnimation(expandAnimation)
+        }
+    }
+
+    fun collapse() {
+        if (measure() && tabs.measuredHeight != 0) {
+            tabs.startAnimation(collapseAnimation)
+        } else {
+            tabs.gone()
+        }
+    }
+
+    /**
+     * Returns true if the view is measured, otherwise query dimensions and check again.
+     */
+    private fun measure(): Boolean {
+        if (height > 0) return true
+        height = tabs.measuredHeight
+        return height > 0
+    }
+}

+ 0 - 141
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt

@@ -1,141 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.support.graphics.drawable.VectorDrawableCompat
-import android.support.v4.app.Fragment
-import android.support.v4.app.FragmentManager
-import android.support.v4.app.FragmentPagerAdapter
-import android.widget.LinearLayout
-import android.widget.TextView
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
-import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
-import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
-import eu.kanade.tachiyomi.ui.manga.track.TrackFragment
-import eu.kanade.tachiyomi.util.SharedData
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.activity_manga.*
-import kotlinx.android.synthetic.main.toolbar.*
-import nucleus.factory.RequiresPresenter
-
-@RequiresPresenter(MangaPresenter::class)
-class MangaActivity : BaseRxActivity<MangaPresenter>() {
-
-    companion object {
-
-        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
-        const val MANGA_EXTRA = "manga"
-        const val FROM_LAUNCHER_EXTRA = "from_launcher"
-        const val INFO_FRAGMENT = 0
-        const val CHAPTERS_FRAGMENT = 1
-        const val TRACK_FRAGMENT = 2
-
-        fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent {
-            SharedData.put(MangaEvent(manga))
-            return Intent(context, MangaActivity::class.java).apply {
-                putExtra(FROM_CATALOGUE_EXTRA, fromCatalogue)
-                putExtra(MANGA_EXTRA, manga.id)
-            }
-        }
-    }
-
-    private lateinit var adapter: MangaDetailAdapter
-
-    var fromCatalogue: Boolean = false
-        private set
-
-    override fun onCreate(savedState: Bundle?) {
-        setAppTheme()
-        super.onCreate(savedState)
-        setContentView(R.layout.activity_manga)
-
-        val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false)
-
-        // Remove any current manga if we are launching from launcher
-        if (fromLauncher) SharedData.remove(MangaEvent::class.java)
-
-        presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
-            val id = intent.getLongExtra(MANGA_EXTRA, 0)
-            val dbManga = presenter.db.getManga(id).executeAsBlocking()
-            if (dbManga != null) {
-                MangaEvent(dbManga)
-            } else {
-                toast(R.string.manga_not_in_db)
-                finish()
-                return
-            }
-        })
-
-        setupToolbar(toolbar)
-
-        fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false)
-
-        adapter = MangaDetailAdapter(supportFragmentManager, this)
-        view_pager.offscreenPageLimit = 3
-        view_pager.adapter = adapter
-
-        tabs.setupWithViewPager(view_pager)
-
-        if (!fromCatalogue)
-            view_pager.currentItem = CHAPTERS_FRAGMENT
-
-        requestPermissionsOnMarshmallow()
-    }
-
-    fun onSetManga(manga: Manga) {
-        setToolbarTitle(manga.title)
-    }
-
-    fun setTrackingIcon(visible: Boolean) {
-        val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return
-        val drawable = if (visible)
-            VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null)
-        else null
-
-        // I had no choice but to use reflection...
-        val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true }
-        val view = field.get(tab) as LinearLayout
-        val textView = view.getChildAt(1) as TextView
-        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
-        textView.compoundDrawablePadding = 4
-    }
-
-    private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity)
-    : FragmentPagerAdapter(fm) {
-
-        private var tabCount = 2
-
-        private val tabTitles = listOf(
-                R.string.manga_detail_tab,
-                R.string.manga_chapters_tab,
-                R.string.manga_tracking_tab)
-                .map { activity.getString(it) }
-
-        init {
-            if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices())
-                tabCount++
-        }
-
-        override fun getCount(): Int {
-            return tabCount
-        }
-
-        override fun getItem(position: Int): Fragment {
-            when (position) {
-                INFO_FRAGMENT -> return MangaInfoFragment.newInstance()
-                CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance()
-                TRACK_FRAGMENT -> return TrackFragment.newInstance()
-                else -> throw Exception("Unknown position")
-            }
-        }
-
-        override fun getPageTitle(position: Int): CharSequence {
-            return tabTitles[position]
-        }
-
-    }
-
-}

+ 186 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -0,0 +1,186 @@
+package eu.kanade.tachiyomi.ui.manga
+
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.os.Build
+import android.os.Bundle
+import android.support.design.widget.TabLayout
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.Router
+import com.bluelinelabs.conductor.RouterTransaction
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter
+import eu.kanade.tachiyomi.ui.base.controller.RxController
+import eu.kanade.tachiyomi.ui.base.controller.TabbedController
+import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
+import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
+import eu.kanade.tachiyomi.ui.manga.track.TrackController
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.manga_controller.view.*
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class MangaController : RxController, TabbedController {
+
+    constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply {
+        putLong(MANGA_EXTRA, manga?.id!!)
+        putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
+    }) {
+        this.manga = manga
+        if (manga != null) {
+            source = Injekt.get<SourceManager>().get(manga.source)
+        }
+    }
+
+    constructor(mangaId: Long) : this(
+            Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
+
+    var manga: Manga? = null
+        private set
+
+    var source: Source? = null
+        private set
+
+    private var adapter: MangaDetailAdapter? = null
+
+    val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
+
+    val chapterCountRelay: BehaviorRelay<Int> = BehaviorRelay.create()
+
+    val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
+
+    override fun getTitle(): String? {
+        return manga?.title
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.manga_controller, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        if (manga == null || source == null) return
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            requestPermissions(arrayOf(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE), 301)
+        }
+
+        with(view) {
+            adapter = MangaDetailAdapter()
+            view_pager.offscreenPageLimit = 3
+            view_pager.adapter = adapter
+
+            if (!fromCatalogue)
+                view_pager.currentItem = CHAPTERS_CONTROLLER
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+    }
+
+    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeStarted(handler, type)
+        if (type.isEnter) {
+            activity?.tabs?.setupWithViewPager(view?.view_pager)
+        }
+    }
+
+    override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
+        super.onChangeEnded(handler, type)
+        if (manga == null || source == null) {
+            activity?.toast(R.string.manga_not_in_db)
+            router.popController(this)
+        }
+    }
+
+    override fun configureTabs(tabs: TabLayout) {
+        with(tabs) {
+            tabGravity = TabLayout.GRAVITY_FILL
+            tabMode = TabLayout.MODE_FIXED
+        }
+    }
+
+    override fun cleanupTabs(tabs: TabLayout) {
+        setTrackingIcon(false)
+    }
+
+    fun setTrackingIcon(visible: Boolean) {
+        val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
+        val drawable = if (visible)
+            VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
+        else null
+
+        // I had no choice but to use reflection...
+        val view = tabField.get(tab) as LinearLayout
+        val textView = view.getChildAt(1) as TextView
+        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
+        textView.compoundDrawablePadding = if (visible) 4 else 0
+    }
+
+    private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
+
+        private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
+
+        private val tabTitles = listOf(
+                R.string.manga_detail_tab,
+                R.string.manga_chapters_tab,
+                R.string.manga_tracking_tab)
+                .map { resources!!.getString(it) }
+
+        override fun getCount(): Int {
+            return tabCount
+        }
+
+        override fun configureRouter(router: Router, position: Int) {
+            if (!router.hasRootController()) {
+                val controller = when (position) {
+                    INFO_CONTROLLER -> MangaInfoController()
+                    CHAPTERS_CONTROLLER -> ChaptersController()
+                    TRACK_CONTROLLER -> TrackController()
+                    else -> error("Wrong position $position")
+                }
+                router.setRoot(RouterTransaction.with(controller))
+            }
+        }
+
+        override fun getPageTitle(position: Int): CharSequence {
+            return tabTitles[position]
+        }
+
+    }
+
+    companion object {
+
+        const val FROM_CATALOGUE_EXTRA = "from_catalogue"
+        const val MANGA_EXTRA = "manga"
+
+        const val INFO_CONTROLLER = 0
+        const val CHAPTERS_CONTROLLER = 1
+        const val TRACK_CONTROLLER = 2
+
+        private val tabField = TabLayout.Tab::class.java.getDeclaredField("mView")
+                .apply { isAccessible = true }
+    }
+
+}

+ 0 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt

@@ -1,5 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga
-
-import eu.kanade.tachiyomi.data.database.models.Manga
-
-class MangaEvent(val manga: Manga)

+ 0 - 55
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -1,55 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga
-
-import android.os.Bundle
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
-import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
-import eu.kanade.tachiyomi.util.SharedData
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import rx.Observable
-import rx.Subscription
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Presenter of [MangaActivity].
- */
-class MangaPresenter : BasePresenter<MangaActivity>() {
-
-    /**
-     * Database helper.
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Tracking manager.
-     */
-    val trackManager: TrackManager by injectLazy()
-
-    /**
-     * Manga associated with this instance.
-     */
-    lateinit var manga: Manga
-
-    var mangaSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        // Prepare a subject to communicate the chapters and info presenters for the chapter count.
-        SharedData.put(ChapterCountEvent())
-        // Prepare a subject to communicate the chapters and info presenters for the chapter favorite.
-        SharedData.put(MangaFavoriteEvent())
-    }
-
-    fun setMangaEvent(event: MangaEvent) {
-        if (mangaSubscription.isNullOrUnsubscribed()) {
-            manga = event.manga
-            mangaSubscription = Observable.just(manga)
-                    .subscribeLatestCache(MangaActivity::onSetManga)
-        }
-    }
-
-}

+ 9 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt

@@ -6,23 +6,13 @@ import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.util.getResourceColor
 import kotlinx.android.synthetic.main.item_chapter.view.*
-import java.text.DateFormat
-import java.text.DecimalFormat
-import java.text.DecimalFormatSymbols
 import java.util.*
 
 class ChapterHolder(
         private val view: View,
-        private val adapter: ChaptersAdapter)
-: FlexibleViewHolder(view, adapter) {
-
-    private val readColor = view.context.getResourceColor(android.R.attr.textColorHint)
-    private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
-    private val bookmarkedColor = view.context.getResourceColor(R.attr.colorAccent)
-    private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
-    private val df = DateFormat.getDateInstance(DateFormat.SHORT)
+        private val adapter: ChaptersAdapter
+) : FlexibleViewHolder(view, adapter) {
 
     init {
         // We need to post a Runnable to show the popup to make sure that the PopupMenu is
@@ -36,19 +26,19 @@ class ChapterHolder(
 
         chapter_title.text = when (manga.displayMode) {
             Manga.DISPLAY_NUMBER -> {
-                val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
-                context.getString(R.string.display_mode_chapter, formattedNumber)
+                val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
+                context.getString(R.string.display_mode_chapter, number)
             }
             else -> chapter.name
         }
 
         // Set correct text color
-        chapter_title.setTextColor(if (chapter.read) readColor else unreadColor)
-        if (chapter.bookmark) chapter_title.setTextColor(bookmarkedColor)
+        chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
+        if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
 
         if (chapter.date_upload > 0) {
-            chapter_date.text = df.format(Date(chapter.date_upload))
-            chapter_date.setTextColor(if (chapter.read) readColor else unreadColor)
+            chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
+            chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
         } else {
             chapter_date.text = ""
         }
@@ -105,7 +95,7 @@ class ChapterHolder(
 
         // Set a listener so we are notified if a menu item is clicked
         popup.setOnMenuItemClickListener { menuItem ->
-            adapter.menuItemListener(adapterPosition, menuItem)
+            adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
             true
         }
 

+ 56 - 49
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt

@@ -1,50 +1,57 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.model.Download
-
-class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
-        Chapter by chapter {
-
-    private var _status: Int = 0
-
-    var status: Int
-        get() = download?.status ?: _status
-        set(value) { _status = value }
-
-    @Transient var download: Download? = null
-
-    val isDownloaded: Boolean
-        get() = status == Download.DOWNLOADED
-
-    override fun getLayoutRes(): Int {
-        return R.layout.item_chapter
-    }
-
-    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder {
-        return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List<Any?>?) {
-        holder.bind(this, manga)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other is ChapterItem) {
-            return chapter.id!! == other.chapter.id!!
-        }
-        return false
-    }
-
-    override fun hashCode(): Int {
-        return chapter.id!!.hashCode()
-    }
-
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.model.Download
+
+class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
+        Chapter by chapter {
+
+    private var _status: Int = 0
+
+    var status: Int
+        get() = download?.status ?: _status
+        set(value) { _status = value }
+
+    @Transient var download: Download? = null
+
+    val isDownloaded: Boolean
+        get() = status == Download.DOWNLOADED
+
+    override fun getLayoutRes(): Int {
+        return R.layout.item_chapter
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
+                                  parent: ViewGroup): ChapterHolder {
+
+        return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: ChapterHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
+        holder.bind(this, manga)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is ChapterItem) {
+            return chapter.id!! == other.chapter.id!!
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return chapter.id!!.hashCode()
+    }
+
 }

+ 45 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt

@@ -1,19 +1,45 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.view.MenuItem
-import eu.davidea.flexibleadapter.FlexibleAdapter
-
-class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChapterItem>(null, fragment, true) {
-
-    var items: List<ChapterItem> = emptyList()
-
-    val menuItemListener: (Int, MenuItem) -> Unit = { position, item ->
-        fragment.onItemMenuClick(position, item)
-    }
-
-    override fun updateDataSet(items: List<ChapterItem>) {
-        this.items = items
-        super.updateDataSet(items.toList())
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.content.Context
+import android.view.MenuItem
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.getResourceColor
+import java.text.DateFormat
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+
+class ChaptersAdapter(
+        controller: ChaptersController,
+        context: Context
+) : FlexibleAdapter<ChapterItem>(null, controller, true) {
+
+    var items: List<ChapterItem> = emptyList()
+
+    val menuItemListener: OnMenuItemClickListener = controller
+
+    val readColor = context.getResourceColor(android.R.attr.textColorHint)
+
+    val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
+
+    val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
+
+    val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
+            .apply { decimalSeparator = '.' })
+
+    val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
+
+    override fun updateDataSet(items: List<ChapterItem>) {
+        this.items = items
+        super.updateDataSet(items.toList())
+    }
+
+    fun indexOf(item: ChapterItem): Int {
+        return items.indexOf(item)
+    }
+
+    interface OnMenuItemClickListener {
+        fun onMenuItemClick(position: Int, item: MenuItem)
+    }
+
+}

+ 470 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt

@@ -0,0 +1,470 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.support.design.widget.Snackbar
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.DividerItemDecoration
+import android.support.v7.widget.LinearLayoutManager
+import android.view.*
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.view.clicks
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+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.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.getCoordinates
+import eu.kanade.tachiyomi.util.snack
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.fragment_manga_chapters.view.*
+import timber.log.Timber
+
+class ChaptersController : NucleusController<ChaptersPresenter>(),
+        ActionMode.Callback,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        ChaptersAdapter.OnMenuItemClickListener,
+        SetDisplayModeDialog.Listener,
+        SetSortingDialog.Listener,
+        DownloadChaptersDialog.Listener,
+        DeleteChaptersDialog.Listener {
+
+    /**
+     * Adapter containing a list of chapters.
+     */
+    private var adapter: ChaptersAdapter? = null
+
+    /**
+     * Action mode for multiple selection.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Selected items. Used to restore selections after a rotation.
+     */
+    private val selectedItems = mutableSetOf<ChapterItem>()
+
+    init {
+        setHasOptionsMenu(true)
+        setOptionsMenuHidden(true)
+    }
+
+    override fun createPresenter(): ChaptersPresenter {
+        val ctrl = parentController as MangaController
+        return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
+                ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_manga_chapters, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        // Init RecyclerView and adapter
+        adapter = ChaptersAdapter(this, view.context)
+
+        with(view) {
+            recycler.adapter = adapter
+            recycler.layoutManager = LinearLayoutManager(context)
+            recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
+            recycler.setHasFixedSize(true)
+            // TODO enable in a future commit
+//             adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent))
+//             adapter.toggleFastScroller()
+
+            swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
+
+            fab.clicks().subscribeUntilDestroy {
+                val item = presenter.getNextUnreadChapter()
+                if (item != null) {
+                    // Create animation listener
+                    val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
+                        override fun onAnimationStart(animation: Animator?) {
+                            openChapter(item.chapter, true)
+                        }
+                    }
+
+                    // Get coordinates and start animation
+                    val coordinates = fab.getCoordinates()
+                    if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
+                        openChapter(item.chapter)
+                    }
+                } else {
+                    context.toast(R.string.no_next_chapter)
+                }
+            }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+        actionMode = null
+    }
+
+    override fun onActivityResumed(activity: Activity) {
+        val view = view ?: return
+
+        // Check if animation view is visible
+        if (view.reveal_view.visibility == View.VISIBLE) {
+            // Show the unReveal effect
+            val coordinates = view.fab.getCoordinates()
+            view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
+        }
+        super.onActivityResumed(activity)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.chapters, menu)
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        // Initialize menu items.
+        val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
+        val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
+        val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
+        val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
+
+        // Set correct checkbox values.
+        menuFilterRead.isChecked = presenter.onlyRead()
+        menuFilterUnread.isChecked = presenter.onlyUnread()
+        menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
+        menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
+
+        if (presenter.onlyRead())
+            //Disable unread filter option if read filter is enabled.
+            menuFilterUnread.isEnabled = false
+        if (presenter.onlyUnread())
+            //Disable read filter option if unread filter is enabled.
+            menuFilterRead.isEnabled = false
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_display_mode -> showDisplayModeDialog()
+            R.id.manga_download -> showDownloadDialog()
+            R.id.action_sorting_mode -> showSortingDialog()
+            R.id.action_filter_unread -> {
+                item.isChecked = !item.isChecked
+                presenter.setUnreadFilter(item.isChecked)
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_filter_read -> {
+                item.isChecked = !item.isChecked
+                presenter.setReadFilter(item.isChecked)
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_filter_downloaded -> {
+                item.isChecked = !item.isChecked
+                presenter.setDownloadedFilter(item.isChecked)
+            }
+            R.id.action_filter_bookmarked -> {
+                item.isChecked = !item.isChecked
+                presenter.setBookmarkedFilter(item.isChecked)
+            }
+            R.id.action_filter_empty -> {
+                presenter.removeFilters()
+                activity?.invalidateOptionsMenu()
+            }
+            R.id.action_sort -> presenter.revertSortOrder()
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    fun onNextChapters(chapters: List<ChapterItem>) {
+        // If the list is empty, fetch chapters from source if the conditions are met
+        // We use presenter chapters instead because they are always unfiltered
+        if (presenter.chapters.isEmpty())
+            initialFetchChapters()
+
+        val adapter = adapter ?: return
+        adapter.updateDataSet(chapters)
+
+        if (selectedItems.isNotEmpty()) {
+            adapter.clearSelection() // we need to start from a clean state, index may have changed
+            createActionModeIfNeeded()
+            selectedItems.forEach { item ->
+                val position = adapter.indexOf(item)
+                if (position != -1 && !adapter.isSelected(position)) {
+                    adapter.toggleSelection(position)
+                }
+            }
+            actionMode?.invalidate()
+        }
+
+    }
+
+    private fun initialFetchChapters() {
+        // Only fetch if this view is from the catalog and it hasn't requested previously
+        if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
+            fetchChaptersFromSource()
+        }
+    }
+
+    fun fetchChaptersFromSource() {
+        view?.swipe_refresh?.isRefreshing = true
+        presenter.fetchChaptersFromSource()
+    }
+
+    fun onFetchChaptersDone() {
+        view?.swipe_refresh?.isRefreshing = false
+    }
+
+    fun onFetchChaptersError(error: Throwable) {
+        view?.swipe_refresh?.isRefreshing = false
+        activity?.toast(error.message)
+    }
+
+    fun onChapterStatusChange(download: Download) {
+        getHolder(download.chapter)?.notifyStatus(download.status)
+    }
+
+    private fun getHolder(chapter: Chapter): ChapterHolder? {
+        return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
+    }
+
+    fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
+        val activity = activity ?: return
+        val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
+        if (hasAnimation) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+        }
+        startActivity(intent)
+    }
+
+    override fun onItemClick(position: Int): Boolean {
+        val adapter = adapter ?: return false
+        val item = adapter.getItem(position) ?: return false
+        if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openChapter(item.chapter)
+            return false
+        }
+    }
+
+    override fun onItemLongClick(position: Int) {
+        createActionModeIfNeeded()
+        toggleSelection(position)
+    }
+
+    // SELECTIONS & ACTION MODE
+
+    private fun toggleSelection(position: Int) {
+        val adapter = adapter ?: return
+        val item = adapter.getItem(position) ?: return
+        adapter.toggleSelection(position)
+        if (adapter.isSelected(position)) {
+            selectedItems.add(item)
+        } else {
+            selectedItems.remove(item)
+        }
+        actionMode?.invalidate()
+    }
+
+    fun getSelectedChapters(): List<ChapterItem> {
+        val adapter = adapter ?: return emptyList()
+        return adapter.selectedPositions.map { adapter.getItem(it) }
+    }
+
+    fun createActionModeIfNeeded() {
+        if (actionMode == null) {
+            actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
+        }
+    }
+
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
+        mode.menuInflater.inflate(R.menu.chapter_selection, menu)
+        adapter?.mode = FlexibleAdapter.MODE_MULTI
+        return true
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = adapter?.selectedItemCount ?: 0
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+        }
+        return false
+    }
+
+    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_select_all -> selectAll()
+            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 -> showDeleteChaptersConfirmationDialog()
+            else -> return false
+        }
+        return true
+    }
+
+    override fun onDestroyActionMode(mode: ActionMode) {
+        adapter?.mode = FlexibleAdapter.MODE_SINGLE
+        adapter?.clearSelection()
+        selectedItems.clear()
+        actionMode = null
+    }
+
+    override fun onMenuItemClick(position: Int, item: MenuItem) {
+        val chapter = adapter?.getItem(position) ?: return
+        val chapters = listOf(chapter)
+
+        when (item.itemId) {
+            R.id.action_download -> downloadChapters(chapters)
+            R.id.action_bookmark -> bookmarkChapters(chapters, true)
+            R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
+            R.id.action_delete -> deleteChapters(chapters)
+            R.id.action_mark_as_read -> markAsRead(chapters)
+            R.id.action_mark_as_unread -> markAsUnread(chapters)
+            R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
+        }
+    }
+
+    // SELECTION MODE ACTIONS
+
+    fun selectAll() {
+        val adapter = adapter ?: return
+        adapter.selectAll()
+        selectedItems.addAll(adapter.items)
+        actionMode?.invalidate()
+    }
+
+    fun markAsRead(chapters: List<ChapterItem>) {
+        presenter.markChaptersRead(chapters, true)
+        if (presenter.preferences.removeAfterMarkedAsRead()) {
+            deleteChapters(chapters)
+        }
+    }
+
+    fun markAsUnread(chapters: List<ChapterItem>) {
+        presenter.markChaptersRead(chapters, false)
+    }
+
+    fun downloadChapters(chapters: List<ChapterItem>) {
+        val view = view
+        destroyActionModeIfNeeded()
+        presenter.downloadChapters(chapters)
+        if (view != null && !presenter.manga.favorite) {
+            view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
+                setAction(R.string.action_add) {
+                    presenter.addToLibrary()
+                }
+            }
+        }
+    }
+
+    private fun showDeleteChaptersConfirmationDialog() {
+        DeleteChaptersDialog(this).showDialog(router)
+    }
+
+    override fun deleteChapters() {
+        deleteChapters(getSelectedChapters())
+    }
+
+    fun markPreviousAsRead(chapter: ChapterItem) {
+        val adapter = adapter ?: return
+        val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
+        val chapterPos = chapters.indexOf(chapter)
+        if (chapterPos != -1) {
+            presenter.markChaptersRead(chapters.take(chapterPos), true)
+        }
+    }
+
+    fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
+        destroyActionModeIfNeeded()
+        presenter.bookmarkChapters(chapters, bookmarked)
+    }
+
+    fun deleteChapters(chapters: List<ChapterItem>) {
+        destroyActionModeIfNeeded()
+        if (chapters.isEmpty()) return
+
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(chapters)
+    }
+
+    fun onChaptersDeleted() {
+        dismissDeletingDialog()
+        adapter?.notifyDataSetChanged()
+    }
+
+    fun onChaptersDeletedError(error: Throwable) {
+        dismissDeletingDialog()
+        Timber.e(error)
+    }
+
+    fun dismissDeletingDialog() {
+        router.popControllerWithTag(DeletingChaptersDialog.TAG)
+    }
+
+    // OVERFLOW MENU DIALOGS
+
+    private fun showDisplayModeDialog() {
+        val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
+        SetDisplayModeDialog(this, preselected).showDialog(router)
+    }
+
+    override fun setDisplayMode(id: Int) {
+        presenter.setDisplayMode(id)
+        adapter?.notifyDataSetChanged()
+    }
+
+    private fun showSortingDialog() {
+        val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
+        SetSortingDialog(this, preselected).showDialog(router)
+    }
+
+    override fun setSorting(id: Int) {
+        presenter.setSorting(id)
+    }
+
+    private fun showDownloadDialog() {
+        DownloadChaptersDialog(this).showDialog(router)
+    }
+
+    override fun downloadChapters(choice: Int) {
+        fun getUnreadChaptersSorted() = presenter.chapters
+                .filter { !it.read && it.status == Download.NOT_DOWNLOADED }
+                .distinctBy { it.name }
+                .sortedByDescending { it.source_order }
+
+        // i = 0: Download 1
+        // i = 1: Download 5
+        // i = 2: Download 10
+        // i = 3: Download unread
+        // i = 4: Download all
+        val chaptersToDownload = when (choice) {
+            0 -> getUnreadChaptersSorted().take(1)
+            1 -> getUnreadChaptersSorted().take(5)
+            2 -> getUnreadChaptersSorted().take(10)
+            3 -> presenter.chapters.filter { !it.read }
+            4 -> presenter.chapters
+            else -> emptyList()
+        }
+
+        if (chaptersToDownload.isNotEmpty()) {
+            downloadChapters(chaptersToDownload)
+        }
+    }
+
+}

+ 0 - 454
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt

@@ -1,454 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.content.Intent
-import android.os.Bundle
-import android.support.design.widget.Snackbar
-import android.support.v4.app.DialogFragment
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.DividerItemDecoration
-import android.support.v7.widget.LinearLayoutManager
-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.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.getCoordinates
-import eu.kanade.tachiyomi.util.snack
-import eu.kanade.tachiyomi.util.toast
-import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
-import kotlinx.android.synthetic.main.fragment_manga_chapters.*
-import nucleus.factory.RequiresPresenter
-import timber.log.Timber
-
-@RequiresPresenter(ChaptersPresenter::class)
-class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(),
-        ActionMode.Callback,
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener {
-
-    companion object {
-        /**
-         * Creates a new instance of this fragment.
-         *
-         * @return a new instance of [ChaptersFragment].
-         */
-        fun newInstance(): ChaptersFragment {
-            return ChaptersFragment()
-        }
-
-    }
-
-    /**
-     * Adapter containing a list of chapters.
-     */
-    private lateinit var adapter: ChaptersAdapter
-
-    /**
-     * Action mode for multiple selection.
-     */
-    private var actionMode: ActionMode? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        setHasOptionsMenu(true)
-    }
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
-        return inflater.inflate(R.layout.fragment_manga_chapters, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedState: Bundle?) {
-        // Init RecyclerView and adapter
-        adapter = ChaptersAdapter(this)
-
-        recycler.adapter = adapter
-        recycler.layoutManager = LinearLayoutManager(activity)
-        recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
-        recycler.setHasFixedSize(true)
-//        TODO enable in a future commit
-//        adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent))
-//        adapter.toggleFastScroller()
-
-        swipe_refresh.setOnRefreshListener { fetchChapters() }
-
-        fab.setOnClickListener {
-            val item = presenter.getNextUnreadChapter()
-            if (item != null) {
-                // Create animation listener
-                val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
-                    override fun onAnimationStart(animation: Animator?) {
-                        openChapter(item.chapter, true)
-                    }
-                }
-
-                // Get coordinates and start animation
-                val coordinates = fab.getCoordinates()
-                if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
-                    openChapter(item.chapter)
-                }
-            } else {
-                context.toast(R.string.no_next_chapter)
-            }
-        }
-    }
-
-    override fun onResume() {
-        // Check if animation view is visible
-        if (reveal_view.visibility == View.VISIBLE) {
-            // Show the unReveal effect
-            val coordinates = fab.getCoordinates()
-            reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
-        }
-        super.onResume()
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.chapters, menu)
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        // Initialize menu items.
-        val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
-        val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
-        val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
-        val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
-
-        // Set correct checkbox values.
-        menuFilterRead.isChecked = presenter.onlyRead()
-        menuFilterUnread.isChecked = presenter.onlyUnread()
-        menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
-        menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
-
-        if (presenter.onlyRead())
-            //Disable unread filter option if read filter is enabled.
-            menuFilterUnread.isEnabled = false
-        if (presenter.onlyUnread())
-            //Disable read filter option if unread filter is enabled.
-            menuFilterRead.isEnabled = false
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_display_mode -> showDisplayModeDialog()
-            R.id.manga_download -> showDownloadDialog()
-            R.id.action_sorting_mode -> showSortingDialog()
-            R.id.action_filter_unread -> {
-                item.isChecked = !item.isChecked
-                presenter.setUnreadFilter(item.isChecked)
-                activity.supportInvalidateOptionsMenu()
-            }
-            R.id.action_filter_read -> {
-                item.isChecked = !item.isChecked
-                presenter.setReadFilter(item.isChecked)
-                activity.supportInvalidateOptionsMenu()
-            }
-            R.id.action_filter_downloaded -> {
-                item.isChecked = !item.isChecked
-                presenter.setDownloadedFilter(item.isChecked)
-            }
-            R.id.action_filter_bookmarked -> {
-                item.isChecked = !item.isChecked
-                presenter.setBookmarkedFilter(item.isChecked)
-            }
-            R.id.action_filter_empty -> {
-                presenter.removeFilters()
-                activity.supportInvalidateOptionsMenu()
-            }
-            R.id.action_sort -> presenter.revertSortOrder()
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    @Suppress("UNUSED_PARAMETER")
-    fun onNextManga(manga: Manga) {
-        // Set initial values
-        activity.supportInvalidateOptionsMenu()
-    }
-
-    fun onNextChapters(chapters: List<ChapterItem>) {
-        // If the list is empty, fetch chapters from source if the conditions are met
-        // We use presenter chapters instead because they are always unfiltered
-        if (presenter.chapters.isEmpty())
-            initialFetchChapters()
-
-        destroyActionModeIfNeeded()
-        adapter.updateDataSet(chapters)
-    }
-
-    private fun initialFetchChapters() {
-        // Only fetch if this view is from the catalog and it hasn't requested previously
-        if (isCatalogueManga && !presenter.hasRequested) {
-            fetchChapters()
-        }
-    }
-
-    fun fetchChapters() {
-        swipe_refresh.isRefreshing = true
-        presenter.fetchChaptersFromSource()
-    }
-
-    fun onFetchChaptersDone() {
-        swipe_refresh.isRefreshing = false
-    }
-
-    fun onFetchChaptersError(error: Throwable) {
-        swipe_refresh.isRefreshing = false
-        context.toast(error.message)
-    }
-
-    val isCatalogueManga: Boolean
-        get() = (activity as MangaActivity).fromCatalogue
-
-    fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
-        val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
-        if (hasAnimation) {
-            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
-        }
-        startActivity(intent)
-    }
-
-    private fun showDisplayModeDialog() {
-        // Get available modes, ids and the selected mode
-        val modes = intArrayOf(R.string.show_title, R.string.show_chapter_number)
-        val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
-        val selectedIndex = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.action_display_mode)
-                .items(modes.map { getString(it) })
-                .itemsIds(ids)
-                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
-                    // Save the new display mode
-                    presenter.setDisplayMode(itemView.id)
-                    // Refresh ui
-                    adapter.notifyItemRangeChanged(0, adapter.itemCount)
-                    true
-                }
-                .show()
-    }
-
-    private fun showSortingDialog() {
-        // Get available modes, ids and the selected mode
-        val modes = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
-        val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
-        val selectedIndex = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.sorting_mode)
-                .items(modes.map { getString(it) })
-                .itemsIds(ids)
-                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
-                    // Save the new sorting mode
-                    presenter.setSorting(itemView.id)
-                    true
-                }
-                .show()
-    }
-
-    private fun showDownloadDialog() {
-        // Get available modes
-        val modes = intArrayOf(R.string.download_1, R.string.download_5, R.string.download_10,
-                R.string.download_unread, R.string.download_all)
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.manga_download)
-                .negativeText(android.R.string.cancel)
-                .items(modes.map { getString(it) })
-                .itemsCallback { _, _, i, _ ->
-
-                    fun getUnreadChaptersSorted() = presenter.chapters
-                            .filter { !it.read && it.status == Download.NOT_DOWNLOADED }
-                            .distinctBy { it.name }
-                            .sortedByDescending { it.source_order }
-
-                    // i = 0: Download 1
-                    // i = 1: Download 5
-                    // i = 2: Download 10
-                    // i = 3: Download unread
-                    // i = 4: Download all
-                    val chaptersToDownload = when (i) {
-                        0 -> getUnreadChaptersSorted().take(1)
-                        1 -> getUnreadChaptersSorted().take(5)
-                        2 -> getUnreadChaptersSorted().take(10)
-                        3 -> presenter.chapters.filter { !it.read }
-                        4 -> presenter.chapters
-                        else -> emptyList()
-                    }
-
-                    if (chaptersToDownload.isNotEmpty()) {
-                        downloadChapters(chaptersToDownload)
-                    }
-                }
-                .show()
-    }
-
-    fun onChapterStatusChange(download: Download) {
-        getHolder(download.chapter)?.notifyStatus(download.status)
-    }
-
-    private fun getHolder(chapter: Chapter): ChapterHolder? {
-        return recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
-    }
-
-    override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
-        mode.menuInflater.inflate(R.menu.chapter_selection, menu)
-        adapter.mode = FlexibleAdapter.MODE_MULTI
-        return true
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
-        return false
-    }
-
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_select_all -> selectAll()
-            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 { _, _ -> deleteChapters(getSelectedChapters()) }
-                        .show()
-            }
-            else -> return false
-        }
-        return true
-    }
-
-    override fun onDestroyActionMode(mode: ActionMode) {
-        adapter.mode = FlexibleAdapter.MODE_SINGLE
-        adapter.clearSelection()
-        actionMode = null
-    }
-
-    fun getSelectedChapters(): List<ChapterItem> {
-        return adapter.selectedPositions.map { adapter.getItem(it) }
-    }
-
-    fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    fun selectAll() {
-        adapter.selectAll()
-        setContextTitle(adapter.selectedItemCount)
-    }
-
-    fun markAsRead(chapters: List<ChapterItem>) {
-        presenter.markChaptersRead(chapters, true)
-        if (presenter.preferences.removeAfterMarkedAsRead()) {
-            deleteChapters(chapters)
-        }
-    }
-
-    fun markAsUnread(chapters: List<ChapterItem>) {
-        presenter.markChaptersRead(chapters, false)
-    }
-
-    fun markPreviousAsRead(chapter: ChapterItem) {
-        val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
-        val chapterPos = chapters.indexOf(chapter)
-        if (chapterPos != -1) {
-            presenter.markChaptersRead(chapters.take(chapterPos), true)
-        }
-    }
-
-    fun downloadChapters(chapters: List<ChapterItem>) {
-        destroyActionModeIfNeeded()
-        presenter.downloadChapters(chapters)
-        if (!presenter.manga.favorite){
-            recycler.snack(getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
-                setAction(R.string.action_add) {
-                    presenter.addToLibrary()
-                }
-            }
-        }
-    }
-
-    fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
-        destroyActionModeIfNeeded()
-        presenter.bookmarkChapters(chapters, bookmarked)
-    }
-
-    fun deleteChapters(chapters: List<ChapterItem>) {
-        destroyActionModeIfNeeded()
-        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
-        presenter.deleteChapters(chapters)
-    }
-
-    fun onChaptersDeleted() {
-        dismissDeletingDialog()
-        adapter.notifyItemRangeChanged(0, adapter.itemCount)
-    }
-
-    fun onChaptersDeletedError(error: Throwable) {
-        dismissDeletingDialog()
-        Timber.e(error)
-    }
-
-    fun dismissDeletingDialog() {
-        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)
-                ?.dismissAllowingStateLoss()
-    }
-
-    override fun onItemClick(position: Int): Boolean {
-        val item = adapter.getItem(position) ?: return false
-        if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openChapter(item.chapter)
-            return false
-        }
-    }
-
-    override fun onItemLongClick(position: Int) {
-        if (actionMode == null)
-            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
-
-        toggleSelection(position)
-    }
-
-    fun onItemMenuClick(position: Int, item: MenuItem) {
-        val chapter = adapter.getItem(position)?.let { listOf(it) } ?: return
-
-        when (item.itemId) {
-            R.id.action_download -> downloadChapters(chapter)
-            R.id.action_bookmark -> bookmarkChapters(chapter, true)
-            R.id.action_remove_bookmark -> bookmarkChapters(chapter, false)
-            R.id.action_delete -> deleteChapters(chapter)
-            R.id.action_mark_as_read -> markAsRead(chapter)
-            R.id.action_mark_as_unread -> markAsUnread(chapter)
-            R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter[0])
-        }
-    }
-
-    private fun toggleSelection(position: Int) {
-        adapter.toggleSelection(position)
-
-        val count = adapter.selectedItemCount
-        if (count == 0) {
-            actionMode?.finish()
-        } else {
-            setContextTitle(count)
-            actionMode?.invalidate()
-        }
-    }
-
-    private fun setContextTitle(count: Int) {
-        actionMode?.title = getString(R.string.label_selected, count)
-    }
-}

+ 415 - 446
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt

@@ -1,446 +1,415 @@
-package eu.kanade.tachiyomi.ui.manga.chapter
-
-import android.os.Bundle
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.Manga
-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.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.manga.MangaEvent
-import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
-import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
-import eu.kanade.tachiyomi.util.SharedData
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import eu.kanade.tachiyomi.util.syncChaptersWithSource
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import timber.log.Timber
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Presenter of [ChaptersFragment].
- */
-class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
-
-    /**
-     * Database helper.
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Source manager.
-     */
-    val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Preferences.
-     */
-    val preferences: PreferencesHelper by injectLazy()
-
-    /**
-     * Downloads manager.
-     */
-    val downloadManager: DownloadManager by injectLazy()
-
-    /**
-     * Active manga.
-     */
-    lateinit var manga: Manga
-        private set
-
-    /**
-     * Source of the manga.
-     */
-    lateinit var source: Source
-        private set
-
-    /**
-     * List of chapters of the manga. It's always unfiltered and unsorted.
-     */
-    var chapters: List<ChapterItem> = emptyList()
-        private set
-
-    /**
-     * Subject of list of chapters to allow updating the view without going to DB.
-     */
-    val chaptersRelay: PublishRelay<List<ChapterItem>>
-            by lazy { PublishRelay.create<List<ChapterItem>>() }
-
-    /**
-     * Whether the chapter list has been requested to the source.
-     */
-    var hasRequested = false
-        private set
-
-    /**
-     * Subscription to retrieve the new list of chapters from the source.
-     */
-    private var fetchChaptersSubscription: Subscription? = null
-
-    /**
-     * Subscription to observe download status changes.
-     */
-    private var observeDownloadsSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        // Find the active manga from the shared data or return.
-        manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
-        source = sourceManager.get(manga.source)!!
-        Observable.just(manga)
-                .subscribeLatestCache(ChaptersFragment::onNextManga)
-
-        // Prepare the relay.
-        chaptersRelay.flatMap { applyChapterFilters(it) }
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(ChaptersFragment::onNextChapters,
-                        { _, error -> Timber.e(error) })
-
-        // Add the subscription that retrieves the chapters from the database, keeps subscribed to
-        // changes, and sends the list of chapters to the relay.
-        add(db.getChapters(manga).asRxObservable()
-                .map { chapters ->
-                    // Convert every chapter to a model.
-                    chapters.map { it.toModel() }
-                }
-                .doOnNext { chapters ->
-                    // Find downloaded chapters
-                    setDownloadedChapters(chapters)
-
-                    // Store the last emission
-                    this.chapters = chapters
-
-                    // Listen for download status changes
-                    observeDownloads()
-
-                    // Emit the number of chapters to the info tab.
-                    SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
-                }
-                .subscribe { chaptersRelay.call(it) })
-    }
-
-    private fun observeDownloads() {
-        observeDownloadsSubscription?.let { remove(it) }
-        observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
-                .observeOn(AndroidSchedulers.mainThread())
-                .filter { download -> download.manga.id == manga.id }
-                .doOnNext { onDownloadStatusChange(it) }
-                .subscribeLatestCache(ChaptersFragment::onChapterStatusChange,
-                        { _, error -> Timber.e(error) })
-    }
-
-    /**
-     * Converts a chapter from the database to an extended model, allowing to store new fields.
-     */
-    private fun Chapter.toModel(): ChapterItem {
-        // Create the model object.
-        val model = ChapterItem(this, manga)
-
-        // Find an active download for this chapter.
-        val download = downloadManager.queue.find { it.chapter.id == id }
-
-        if (download != null) {
-            // If there's an active download, assign it.
-            model.download = download
-        }
-        return model
-    }
-
-    /**
-     * Finds and assigns the list of downloaded chapters.
-     *
-     * @param chapters the list of chapter from the database.
-     */
-    private fun setDownloadedChapters(chapters: List<ChapterItem>) {
-        val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
-        val cached = mutableMapOf<Chapter, String>()
-        files.mapNotNull { it.name }
-                .mapNotNull { name -> chapters.find {
-                    name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) }
-                } }
-                .forEach { it.status = Download.DOWNLOADED }
-    }
-
-    /**
-     * Requests an updated list of chapters from the source.
-     */
-    fun fetchChaptersFromSource() {
-        hasRequested = true
-
-        if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
-        fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
-                .subscribeOn(Schedulers.io())
-                .map { syncChaptersWithSource(db, it, manga, source) }
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, _ ->
-                    view.onFetchChaptersDone()
-                }, ChaptersFragment::onFetchChaptersError)
-    }
-
-    /**
-     * Updates the UI after applying the filters.
-     */
-    private fun refreshChapters() {
-        chaptersRelay.call(chapters)
-    }
-
-    /**
-     * Applies the view filters to the list of chapters obtained from the database.
-     * @param chapters the list of chapters from the database
-     * @return an observable of the list of chapters filtered and sorted.
-     */
-    private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
-        var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
-        if (onlyUnread()) {
-            observable = observable.filter { !it.read }
-        }
-        else if (onlyRead()) {
-            observable = observable.filter { it.read }
-        }
-        if (onlyDownloaded()) {
-            observable = observable.filter { it.isDownloaded }
-        }
-        if (onlyBookmarked()) {
-            observable = observable.filter { it.bookmark }
-        }
-        val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
-            Manga.SORTING_SOURCE -> when (sortDescending()) {
-                true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
-                false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
-            }
-            Manga.SORTING_NUMBER -> when (sortDescending()) {
-                true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
-                false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
-            }
-            else -> throw NotImplementedError("Unimplemented sorting method")
-        }
-        return observable.toSortedList(sortFunction)
-    }
-
-    /**
-     * Called when a download for the active manga changes status.
-     * @param download the download whose status changed.
-     */
-    fun onDownloadStatusChange(download: Download) {
-        // Assign the download to the model object.
-        if (download.status == Download.QUEUE) {
-            chapters.find { it.id == download.chapter.id }?.let {
-                if (it.download == null) {
-                    it.download = download
-                }
-            }
-        }
-
-        // Force UI update if downloaded filter active and download finished.
-        if (onlyDownloaded() && download.status == Download.DOWNLOADED)
-            refreshChapters()
-    }
-
-    /**
-     * Returns the next unread chapter or null if everything is read.
-     */
-    fun getNextUnreadChapter(): ChapterItem? {
-        return chapters.sortedByDescending { it.source_order }.find { !it.read }
-    }
-
-    /**
-     * Mark the selected chapter list as read/unread.
-     * @param selectedChapters the list of selected chapters.
-     * @param read whether to mark chapters as read or unread.
-     */
-    fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
-        Observable.from(selectedChapters)
-                .doOnNext { chapter ->
-                    chapter.read = read
-                    if (!read) {
-                        chapter.last_page_read = 0
-                    }
-                }
-                .toList()
-                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Downloads the given list of chapters with the manager.
-     * @param chapters the list of chapters to download.
-     */
-    fun downloadChapters(chapters: List<ChapterItem>) {
-        DownloadService.start(context)
-        downloadManager.downloadChapters(manga, chapters)
-    }
-
-    /**
-     * Bookmarks the given list of chapters.
-     * @param selectedChapters the list of chapters to bookmark.
-     */
-    fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
-        Observable.from(selectedChapters)
-                .doOnNext { chapter ->
-                    chapter.bookmark = bookmarked
-                }
-                .toList()
-                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
-                .subscribeOn(Schedulers.io())
-                .subscribe()
-    }
-
-    /**
-     * Deletes the given list of chapter.
-     * @param chapters the list of chapters to delete.
-     */
-    fun deleteChapters(chapters: List<ChapterItem>) {
-        Observable.from(chapters)
-                .doOnNext { deleteChapter(it) }
-                .toList()
-                .doOnNext { if (onlyDownloaded()) refreshChapters() }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, _ ->
-                    view.onChaptersDeleted()
-                }, ChaptersFragment::onChaptersDeletedError)
-    }
-
-    /**
-     * Deletes a chapter from disk. This method is called in a background thread.
-     * @param chapter the chapter to delete.
-     */
-    private fun deleteChapter(chapter: ChapterItem) {
-        downloadManager.queue.remove(chapter)
-        downloadManager.deleteChapter(source, manga, chapter)
-        chapter.status = Download.NOT_DOWNLOADED
-        chapter.download = null
-    }
-
-    /**
-     * Reverses the sorting and requests an UI update.
-     */
-    fun revertSortOrder() {
-        manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the read filter and requests an UI update.
-     * @param onlyUnread whether to display only unread chapters or all chapters.
-     */
-    fun setUnreadFilter(onlyUnread: Boolean) {
-        manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the read filter and requests an UI update.
-     * @param onlyRead whether to display only read chapters or all chapters.
-     */
-    fun setReadFilter(onlyRead: Boolean) {
-        manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the download filter and requests an UI update.
-     * @param onlyDownloaded whether to display only downloaded chapters or all chapters.
-     */
-    fun setDownloadedFilter(onlyDownloaded: Boolean) {
-        manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Sets the bookmark filter and requests an UI update.
-     * @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
-     */
-    fun setBookmarkedFilter(onlyBookmarked: Boolean) {
-        manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Removes all filters and requests an UI update.
-     */
-    fun removeFilters() {
-        manga.readFilter = Manga.SHOW_ALL
-        manga.downloadedFilter = Manga.SHOW_ALL
-        manga.bookmarkedFilter = Manga.SHOW_ALL
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Adds manga to library
-     */
-    fun addToLibrary() {
-        SharedData.get(MangaFavoriteEvent::class.java)?.call(true)
-    }
-
-    /**
-     * Sets the active display mode.
-     * @param mode the mode to set.
-     */
-    fun setDisplayMode(mode: Int) {
-        manga.displayMode = mode
-        db.updateFlags(manga).executeAsBlocking()
-    }
-
-    /**
-     * Sets the sorting method and requests an UI update.
-     * @param sort the sorting mode.
-     */
-    fun setSorting(sort: Int) {
-        manga.sorting = sort
-        db.updateFlags(manga).executeAsBlocking()
-        refreshChapters()
-    }
-
-    /**
-     * Whether the display only downloaded filter is enabled.
-     */
-    fun onlyDownloaded(): Boolean {
-        return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
-    }
-
-    /**
-     * Whether the display only downloaded filter is enabled.
-     */
-    fun onlyBookmarked(): Boolean {
-        return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
-    }
-
-    /**
-     * Whether the display only unread filter is enabled.
-     */
-    fun onlyUnread(): Boolean {
-        return manga.readFilter == Manga.SHOW_UNREAD
-    }
-
-    /**
-     * Whether the display only read filter is enabled.
-     */
-    fun onlyRead(): Boolean {
-        return manga.readFilter == Manga.SHOW_READ
-    }
-
-    /**
-     * Whether the sorting method is descending or ascending.
-     */
-    fun sortDescending(): Boolean {
-        return manga.sortDescending()
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.os.Bundle
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+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.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import eu.kanade.tachiyomi.util.syncChaptersWithSource
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * Presenter of [ChaptersController].
+ */
+class ChaptersPresenter(
+        val manga: Manga,
+        val source: Source,
+        private val chapterCountRelay: BehaviorRelay<Int>,
+        private val mangaFavoriteRelay: PublishRelay<Boolean>,
+        val preferences: PreferencesHelper = Injekt.get(),
+        private val db: DatabaseHelper = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get()
+) : BasePresenter<ChaptersController>() {
+
+    private val context = preferences.context
+
+    /**
+     * List of chapters of the manga. It's always unfiltered and unsorted.
+     */
+    var chapters: List<ChapterItem> = emptyList()
+        private set
+
+    /**
+     * Subject of list of chapters to allow updating the view without going to DB.
+     */
+    val chaptersRelay: PublishRelay<List<ChapterItem>>
+            by lazy { PublishRelay.create<List<ChapterItem>>() }
+
+    /**
+     * Whether the chapter list has been requested to the source.
+     */
+    var hasRequested = false
+        private set
+
+    /**
+     * Subscription to retrieve the new list of chapters from the source.
+     */
+    private var fetchChaptersSubscription: Subscription? = null
+
+    /**
+     * Subscription to observe download status changes.
+     */
+    private var observeDownloadsSubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+
+        // Prepare the relay.
+        chaptersRelay.flatMap { applyChapterFilters(it) }
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(ChaptersController::onNextChapters,
+                        { _, error -> Timber.e(error) })
+
+        // Add the subscription that retrieves the chapters from the database, keeps subscribed to
+        // changes, and sends the list of chapters to the relay.
+        add(db.getChapters(manga).asRxObservable()
+                .map { chapters ->
+                    // Convert every chapter to a model.
+                    chapters.map { it.toModel() }
+                }
+                .doOnNext { chapters ->
+                    // Find downloaded chapters
+                    setDownloadedChapters(chapters)
+
+                    // Store the last emission
+                    this.chapters = chapters
+
+                    // Listen for download status changes
+                    observeDownloads()
+
+                    // Emit the number of chapters to the info tab.
+                    chapterCountRelay.call(chapters.size)
+                }
+                .subscribe { chaptersRelay.call(it) })
+    }
+
+    private fun observeDownloads() {
+        observeDownloadsSubscription?.let { remove(it) }
+        observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+                .filter { download -> download.manga.id == manga.id }
+                .doOnNext { onDownloadStatusChange(it) }
+                .subscribeLatestCache(ChaptersController::onChapterStatusChange,
+                        { _, error -> Timber.e(error) })
+    }
+
+    /**
+     * Converts a chapter from the database to an extended model, allowing to store new fields.
+     */
+    private fun Chapter.toModel(): ChapterItem {
+        // Create the model object.
+        val model = ChapterItem(this, manga)
+
+        // Find an active download for this chapter.
+        val download = downloadManager.queue.find { it.chapter.id == id }
+
+        if (download != null) {
+            // If there's an active download, assign it.
+            model.download = download
+        }
+        return model
+    }
+
+    /**
+     * Finds and assigns the list of downloaded chapters.
+     *
+     * @param chapters the list of chapter from the database.
+     */
+    private fun setDownloadedChapters(chapters: List<ChapterItem>) {
+        val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
+        val cached = mutableMapOf<Chapter, String>()
+        files.mapNotNull { it.name }
+                .mapNotNull { name -> chapters.find {
+                    name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) }
+                } }
+                .forEach { it.status = Download.DOWNLOADED }
+    }
+
+    /**
+     * Requests an updated list of chapters from the source.
+     */
+    fun fetchChaptersFromSource() {
+        hasRequested = true
+
+        if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
+        fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
+                .subscribeOn(Schedulers.io())
+                .map { syncChaptersWithSource(db, it, manga, source) }
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, _ ->
+                    view.onFetchChaptersDone()
+                }, ChaptersController::onFetchChaptersError)
+    }
+
+    /**
+     * Updates the UI after applying the filters.
+     */
+    private fun refreshChapters() {
+        chaptersRelay.call(chapters)
+    }
+
+    /**
+     * Applies the view filters to the list of chapters obtained from the database.
+     * @param chapters the list of chapters from the database
+     * @return an observable of the list of chapters filtered and sorted.
+     */
+    private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
+        var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
+        if (onlyUnread()) {
+            observable = observable.filter { !it.read }
+        }
+        else if (onlyRead()) {
+            observable = observable.filter { it.read }
+        }
+        if (onlyDownloaded()) {
+            observable = observable.filter { it.isDownloaded }
+        }
+        if (onlyBookmarked()) {
+            observable = observable.filter { it.bookmark }
+        }
+        val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
+            Manga.SORTING_SOURCE -> when (sortDescending()) {
+                true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
+                false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
+            }
+            Manga.SORTING_NUMBER -> when (sortDescending()) {
+                true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
+                false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
+            }
+            else -> throw NotImplementedError("Unimplemented sorting method")
+        }
+        return observable.toSortedList(sortFunction)
+    }
+
+    /**
+     * Called when a download for the active manga changes status.
+     * @param download the download whose status changed.
+     */
+    fun onDownloadStatusChange(download: Download) {
+        // Assign the download to the model object.
+        if (download.status == Download.QUEUE) {
+            chapters.find { it.id == download.chapter.id }?.let {
+                if (it.download == null) {
+                    it.download = download
+                }
+            }
+        }
+
+        // Force UI update if downloaded filter active and download finished.
+        if (onlyDownloaded() && download.status == Download.DOWNLOADED)
+            refreshChapters()
+    }
+
+    /**
+     * Returns the next unread chapter or null if everything is read.
+     */
+    fun getNextUnreadChapter(): ChapterItem? {
+        return chapters.sortedByDescending { it.source_order }.find { !it.read }
+    }
+
+    /**
+     * Mark the selected chapter list as read/unread.
+     * @param selectedChapters the list of selected chapters.
+     * @param read whether to mark chapters as read or unread.
+     */
+    fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
+        Observable.from(selectedChapters)
+                .doOnNext { chapter ->
+                    chapter.read = read
+                    if (!read) {
+                        chapter.last_page_read = 0
+                    }
+                }
+                .toList()
+                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Downloads the given list of chapters with the manager.
+     * @param chapters the list of chapters to download.
+     */
+    fun downloadChapters(chapters: List<ChapterItem>) {
+        DownloadService.start(context)
+        downloadManager.downloadChapters(manga, chapters)
+    }
+
+    /**
+     * Bookmarks the given list of chapters.
+     * @param selectedChapters the list of chapters to bookmark.
+     */
+    fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
+        Observable.from(selectedChapters)
+                .doOnNext { chapter ->
+                    chapter.bookmark = bookmarked
+                }
+                .toList()
+                .flatMap { db.updateChaptersProgress(it).asRxObservable() }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    /**
+     * Deletes the given list of chapter.
+     * @param chapters the list of chapters to delete.
+     */
+    fun deleteChapters(chapters: List<ChapterItem>) {
+        Observable.from(chapters)
+                .doOnNext { deleteChapter(it) }
+                .toList()
+                .doOnNext { if (onlyDownloaded()) refreshChapters() }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, _ ->
+                    view.onChaptersDeleted()
+                }, ChaptersController::onChaptersDeletedError)
+    }
+
+    /**
+     * Deletes a chapter from disk. This method is called in a background thread.
+     * @param chapter the chapter to delete.
+     */
+    private fun deleteChapter(chapter: ChapterItem) {
+        downloadManager.queue.remove(chapter)
+        downloadManager.deleteChapter(source, manga, chapter)
+        chapter.status = Download.NOT_DOWNLOADED
+        chapter.download = null
+    }
+
+    /**
+     * Reverses the sorting and requests an UI update.
+     */
+    fun revertSortOrder() {
+        manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the read filter and requests an UI update.
+     * @param onlyUnread whether to display only unread chapters or all chapters.
+     */
+    fun setUnreadFilter(onlyUnread: Boolean) {
+        manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the read filter and requests an UI update.
+     * @param onlyRead whether to display only read chapters or all chapters.
+     */
+    fun setReadFilter(onlyRead: Boolean) {
+        manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the download filter and requests an UI update.
+     * @param onlyDownloaded whether to display only downloaded chapters or all chapters.
+     */
+    fun setDownloadedFilter(onlyDownloaded: Boolean) {
+        manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Sets the bookmark filter and requests an UI update.
+     * @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
+     */
+    fun setBookmarkedFilter(onlyBookmarked: Boolean) {
+        manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Removes all filters and requests an UI update.
+     */
+    fun removeFilters() {
+        manga.readFilter = Manga.SHOW_ALL
+        manga.downloadedFilter = Manga.SHOW_ALL
+        manga.bookmarkedFilter = Manga.SHOW_ALL
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Adds manga to library
+     */
+    fun addToLibrary() {
+        mangaFavoriteRelay.call(true)
+    }
+
+    /**
+     * Sets the active display mode.
+     * @param mode the mode to set.
+     */
+    fun setDisplayMode(mode: Int) {
+        manga.displayMode = mode
+        db.updateFlags(manga).executeAsBlocking()
+    }
+
+    /**
+     * Sets the sorting method and requests an UI update.
+     * @param sort the sorting mode.
+     */
+    fun setSorting(sort: Int) {
+        manga.sorting = sort
+        db.updateFlags(manga).executeAsBlocking()
+        refreshChapters()
+    }
+
+    /**
+     * Whether the display only downloaded filter is enabled.
+     */
+    fun onlyDownloaded(): Boolean {
+        return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
+    }
+
+    /**
+     * Whether the display only downloaded filter is enabled.
+     */
+    fun onlyBookmarked(): Boolean {
+        return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
+    }
+
+    /**
+     * Whether the display only unread filter is enabled.
+     */
+    fun onlyUnread(): Boolean {
+        return manga.readFilter == Manga.SHOW_UNREAD
+    }
+
+    /**
+     * Whether the display only read filter is enabled.
+     */
+    fun onlyRead(): Boolean {
+        return manga.readFilter == Manga.SHOW_READ
+    }
+
+    /**
+     * Whether the sorting method is descending or ascending.
+     */
+    fun sortDescending(): Boolean {
+        return manga.sortDescending()
+    }
+
+}

+ 32 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt

@@ -0,0 +1,32 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : DeleteChaptersDialog.Listener {
+
+    constructor(target: T) : this() {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .content(R.string.confirm_delete_chapters)
+                .positiveText(android.R.string.yes)
+                .negativeText(android.R.string.no)
+                .onPositive { _, _ ->
+                    (targetController as? Listener)?.deleteChapters()
+                }
+                .show()
+    }
+
+    interface Listener {
+        fun deleteChapters()
+    }
+
+}

+ 27 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt

@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Router
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
+
+    companion object {
+        const val TAG = "deleting_dialog"
+    }
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .progress(true, 0)
+                .content(R.string.deleting)
+                .build()
+    }
+
+    override fun showDialog(router: Router) {
+        showDialog(router, TAG)
+    }
+
+}

+ 42 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt

@@ -0,0 +1,42 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : DownloadChaptersDialog.Listener {
+
+    constructor(target: T) : this() {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+
+        val choices = intArrayOf(
+                R.string.download_1,
+                R.string.download_5,
+                R.string.download_10,
+                R.string.download_unread,
+                R.string.download_all
+        ).map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.manga_download)
+                .negativeText(android.R.string.cancel)
+                .items(choices)
+                .itemsCallback { _, _, position, _ ->
+                    (targetController as? Listener)?.downloadChapters(position)
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun downloadChapters(choice: Int)
+    }
+
+}

+ 43 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt

@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class SetDisplayModeDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : SetDisplayModeDialog.Listener {
+
+    private val selectedIndex = args.getInt("selected", -1)
+
+    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
+        putInt("selected", selectedIndex)
+    }) {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+        val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
+        val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number)
+                .map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.action_display_mode)
+                .items(choices)
+                .itemsIds(ids)
+                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
+                    (targetController as? Listener)?.setDisplayMode(itemView.id)
+                    true
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun setDisplayMode(id: Int)
+    }
+
+}

+ 43 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt

@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.manga.chapter
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class SetSortingDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : SetSortingDialog.Listener {
+
+    private val selectedIndex = args.getInt("selected", -1)
+
+    constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
+        putInt("selected", selectedIndex)
+    }) {
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+        val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
+        val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
+                .map { activity.getString(it) }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.sorting_mode)
+                .items(choices)
+                .itemsIds(ids)
+                .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
+                    (targetController as? Listener)?.setSorting(itemView.id)
+                    true
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun setSorting(id: Int)
+    }
+
+}

+ 0 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt

@@ -1,16 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import rx.Observable
-import rx.subjects.BehaviorSubject
-
-class ChapterCountEvent {
-
-    private val subject = BehaviorSubject.create<Int>()
-
-    val observable: Observable<Int>
-        get() = subject
-
-    fun emit(count: Int) {
-        subject.onNext(count)
-    }
-}

+ 0 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt

@@ -1,16 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import com.jakewharton.rxrelay.PublishRelay
-import rx.Observable
-
-class MangaFavoriteEvent {
-
-    private val subject = PublishRelay.create<Boolean>()
-
-    val observable: Observable<Boolean>
-        get() = subject
-
-    fun call(favorite: Boolean) {
-        subject.call(favorite)
-    }
-}

+ 399 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt

@@ -0,0 +1,399 @@
+package eu.kanade.tachiyomi.ui.manga.info
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.support.customtabs.CustomTabsIntent
+import android.view.*
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bumptech.glide.BitmapRequestBuilder
+import com.bumptech.glide.BitmapTypeRequest
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.view.clicks
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.snack
+import eu.kanade.tachiyomi.util.toast
+import jp.wasabeef.glide.transformations.CropCircleTransformation
+import jp.wasabeef.glide.transformations.CropSquareTransformation
+import jp.wasabeef.glide.transformations.MaskTransformation
+import jp.wasabeef.glide.transformations.RoundedCornersTransformation
+import kotlinx.android.synthetic.main.fragment_manga_info.view.*
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import rx.subscriptions.Subscriptions
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Fragment that shows manga information.
+ * Uses R.layout.fragment_manga_info.
+ * UI related actions should be called from here.
+ */
+class MangaInfoController : NucleusController<MangaInfoPresenter>(),
+        ChangeMangaCategoriesDialog.Listener {
+
+    /**
+     * Preferences helper.
+     */
+    private val preferences: PreferencesHelper by injectLazy()
+
+    init {
+        setHasOptionsMenu(true)
+        setOptionsMenuHidden(true)
+    }
+
+    override fun createPresenter(): MangaInfoPresenter {
+        val ctrl = parentController as MangaController
+        return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
+                ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_manga_info, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        with(view) {
+            // Set onclickListener to toggle favorite when FAB clicked.
+            fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
+
+            // Set SwipeRefresh to refresh manga data.
+            swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
+        }
+
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.manga_info, menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.action_open_in_browser -> openInBrowser()
+            R.id.action_share -> shareManga()
+            R.id.action_add_to_home_screen -> addToHomeScreen()
+            else -> return super.onOptionsItemSelected(item)
+        }
+        return true
+    }
+
+    /**
+     * Check if manga is initialized.
+     * If true update view with manga information,
+     * if false fetch manga information
+     *
+     * @param manga  manga object containing information about manga.
+     * @param source the source of the manga.
+     */
+    fun onNextManga(manga: Manga, source: Source) {
+        if (manga.initialized) {
+            // Update view.
+            setMangaInfo(manga, source)
+        } else {
+            // Initialize manga.
+            fetchMangaFromSource()
+        }
+    }
+
+    /**
+     * Update the view with manga information.
+     *
+     * @param manga manga object containing information about manga.
+     * @param source the source of the manga.
+     */
+    private fun setMangaInfo(manga: Manga, source: Source?) {
+        val view = view ?: return
+        with(view) {
+            // Update artist TextView.
+            manga_artist.text = manga.artist
+
+            // Update author TextView.
+            manga_author.text = manga.author
+
+            // If manga source is known update source TextView.
+            if (source != null) {
+                manga_source.text = source.toString()
+            }
+
+            // Update genres TextView.
+            manga_genres.text = manga.genre
+
+            // Update status TextView.
+            manga_status.setText(when (manga.status) {
+                SManga.ONGOING -> R.string.ongoing
+                SManga.COMPLETED -> R.string.completed
+                SManga.LICENSED -> R.string.licensed
+                else -> R.string.unknown
+            })
+
+            // Update description TextView.
+            manga_summary.text = manga.description
+
+            // Set the favorite drawable to the correct one.
+            setFavoriteDrawable(manga.favorite)
+
+            // Set cover if it wasn't already.
+            if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
+                Glide.with(context)
+                        .load(manga)
+                        .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                        .centerCrop()
+                        .into(manga_cover)
+
+                Glide.with(context)
+                        .load(manga)
+                        .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                        .centerCrop()
+                        .into(backdrop)
+            }
+        }
+    }
+
+    /**
+     * Update chapter count TextView.
+     *
+     * @param count number of chapters.
+     */
+    fun setChapterCount(count: Int) {
+        view?.manga_chapters?.text = count.toString()
+    }
+
+    /**
+     * Toggles the favorite status and asks for confirmation to delete downloaded chapters.
+     */
+    fun toggleFavorite() {
+        val view = view
+
+        val isNowFavorite = presenter.toggleFavorite()
+        if (view != null && !isNowFavorite && presenter.hasDownloads()) {
+            view.snack(view.context.getString(R.string.delete_downloads_for_manga)) {
+                setAction(R.string.action_delete) {
+                    presenter.deleteDownloads()
+                }
+            }
+        }
+    }
+
+    /**
+     * Open the manga in browser.
+     */
+    fun openInBrowser() {
+        val context = view?.context ?: return
+        val source = presenter.source as? HttpSource ?: return
+
+        try {
+            val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString())
+            val intent = CustomTabsIntent.Builder()
+                    .setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
+                    .build()
+            intent.launchUrl(activity, url)
+        } catch (e: Exception) {
+            context.toast(e.message)
+        }
+    }
+
+    /**
+     * Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
+     */
+    private fun shareManga() {
+        val context = view?.context ?: return
+
+        val source = presenter.source as? HttpSource ?: return
+        try {
+            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
+            val title = presenter.manga.title
+            val intent = Intent(Intent.ACTION_SEND).apply {
+                type = "text/plain"
+                putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_text, title, url))
+            }
+            startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
+        } catch (e: Exception) {
+            context.toast(e.message)
+        }
+    }
+
+    /**
+     * Update FAB with correct drawable.
+     *
+     * @param isFavorite determines if manga is favorite or not.
+     */
+    private fun setFavoriteDrawable(isFavorite: Boolean) {
+        // Set the Favorite drawable to the correct one.
+        // Border drawable if false, filled drawable if true.
+        view?.fab_favorite?.setImageResource(if (isFavorite)
+            R.drawable.ic_bookmark_white_24dp
+        else
+            R.drawable.ic_bookmark_border_white_24dp)
+    }
+
+    /**
+     * Start fetching manga information from source.
+     */
+    private fun fetchMangaFromSource() {
+        setRefreshing(true)
+        // Call presenter and start fetching manga information
+        presenter.fetchMangaFromSource()
+    }
+
+
+    /**
+     * Update swipe refresh to stop showing refresh in progress spinner.
+     */
+    fun onFetchMangaDone() {
+        setRefreshing(false)
+    }
+
+    /**
+     * Update swipe refresh to start showing refresh in progress spinner.
+     */
+    fun onFetchMangaError() {
+        setRefreshing(false)
+    }
+
+    /**
+     * Set swipe refresh status.
+     *
+     * @param value whether it should be refreshing or not.
+     */
+    private fun setRefreshing(value: Boolean) {
+        view?.swipe_refresh?.isRefreshing = value
+    }
+
+    /**
+     * Called when the fab is clicked.
+     */
+    private fun onFabClick() {
+        val manga = presenter.manga
+        toggleFavorite()
+        if (manga.favorite) {
+            val categories = presenter.getCategories()
+            val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
+            if (defaultCategory != null) {
+                presenter.moveMangaToCategory(manga, defaultCategory)
+            } else if (categories.size <= 1) { // default or the one from the user
+                presenter.moveMangaToCategory(manga, categories.firstOrNull())
+            } else {
+                val ids = presenter.getMangaCategoryIds(manga)
+                val preselected = ids.mapNotNull { id ->
+                    categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
+                }.toTypedArray()
+
+                ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
+                        .showDialog(router)
+            }
+        }
+    }
+
+    override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
+        val manga = mangas.firstOrNull() ?: return
+        presenter.moveMangaToCategories(manga, categories)
+    }
+
+    /**
+     * Add the manga to the home screen
+     */
+    fun addToHomeScreen() {
+        val activity = activity ?: return
+        val mangaControllerArgs = parentController?.args ?: return
+
+        val shortcutIntent = activity.intent
+                .setAction(MainActivity.SHORTCUT_MANGA)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                .putExtra(MangaController.MANGA_EXTRA,
+                        mangaControllerArgs.getLong(MangaController.MANGA_EXTRA))
+
+        val addIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT")
+                .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
+
+        //Set shortcut title
+        val dialog = MaterialDialog.Builder(activity)
+                .title(R.string.shortcut_title)
+                .input("", presenter.manga.title, { _, text ->
+                    //Set shortcut title
+                    addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString())
+
+                    reshapeIconBitmap(addIntent,
+                            Glide.with(activity).load(presenter.manga).asBitmap())
+                })
+                .negativeText(android.R.string.cancel)
+                .show()
+
+        untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() })
+    }
+
+    fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest<out Any>) {
+        val activity = activity ?: return
+
+        val modes = intArrayOf(R.string.circular_icon,
+                R.string.rounded_icon,
+                R.string.square_icon,
+                R.string.star_icon)
+
+        fun BitmapRequestBuilder<out Any, Bitmap>.toIcon(): Bitmap {
+            return this.into(96, 96).get()
+        }
+
+        // i = 0: Circular icon
+        // i = 1: Rounded icon
+        // i = 2: Square icon
+        // i = 3: Star icon (because boredom)
+        fun getIcon(i: Int): Bitmap? {
+            return when (i) {
+                0 -> request.transform(CropCircleTransformation(activity)).toIcon()
+                1 -> request.transform(RoundedCornersTransformation(activity, 5, 0)).toIcon()
+                2 -> request.transform(CropSquareTransformation(activity)).toIcon()
+                3 -> request.transform(CenterCrop(activity),
+                        MaskTransformation(activity, R.drawable.mask_star)).toIcon()
+                else -> null
+            }
+        }
+
+        val dialog = MaterialDialog.Builder(activity)
+                .title(R.string.icon_shape)
+                .negativeText(android.R.string.cancel)
+                .items(modes.map { activity.getString(it) })
+                .itemsCallback { _, _, i, _ ->
+                    Observable.fromCallable { getIcon(i) }
+                            .subscribeOn(Schedulers.io())
+                            .observeOn(AndroidSchedulers.mainThread())
+                            .subscribe({ icon ->
+                                if (icon != null) createShortcut(addIntent, icon)
+                            }, {
+                                activity.toast(R.string.icon_creation_fail)
+                            })
+                }
+                .show()
+
+        untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() })
+    }
+
+    fun createShortcut(addIntent: Intent, icon: Bitmap) {
+        val activity = activity ?: return
+
+        //Send shortcut intent
+        addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon)
+        activity.sendBroadcast(addIntent)
+        //Go to launcher to show this shiny new shortcut!
+        val startMain = Intent(Intent.ACTION_MAIN)
+        startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        startActivity(startMain)
+    }
+
+}

+ 0 - 393
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt

@@ -1,393 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import android.content.Intent
-import android.graphics.Bitmap
-import android.net.Uri
-import android.os.Bundle
-import android.support.customtabs.CustomTabsIntent
-import android.view.*
-import android.widget.Toast
-import com.afollestad.materialdialogs.MaterialDialog
-import com.bumptech.glide.BitmapRequestBuilder
-import com.bumptech.glide.BitmapTypeRequest
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.util.getResourceColor
-import eu.kanade.tachiyomi.util.snack
-import eu.kanade.tachiyomi.util.toast
-import jp.wasabeef.glide.transformations.CropCircleTransformation
-import jp.wasabeef.glide.transformations.CropSquareTransformation
-import jp.wasabeef.glide.transformations.MaskTransformation
-import jp.wasabeef.glide.transformations.RoundedCornersTransformation
-import kotlinx.android.synthetic.main.fragment_manga_info.*
-import nucleus.factory.RequiresPresenter
-import rx.Observable
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Fragment that shows manga information.
- * Uses R.layout.fragment_manga_info.
- * UI related actions should be called from here.
- */
-@RequiresPresenter(MangaInfoPresenter::class)
-class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
-
-    companion object {
-        /**
-         * Create new instance of MangaInfoFragment.
-         *
-         * @return MangaInfoFragment.
-         */
-        fun newInstance(): MangaInfoFragment {
-            return MangaInfoFragment()
-        }
-
-    }
-
-    /**
-     * Preferences helper.
-     */
-    private val preferences: PreferencesHelper by injectLazy()
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-        setHasOptionsMenu(true)
-    }
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
-        return inflater.inflate(R.layout.fragment_manga_info, container, false)
-    }
-
-    override fun onViewCreated(view: View?, savedState: Bundle?) {
-        // Set onclickListener to toggle favorite when FAB clicked.
-        fab_favorite.setOnClickListener {
-            if(!presenter.manga.favorite) {
-                val defaultCategory = presenter.getCategories().find { it.id == preferences.defaultCategory()}
-                if(defaultCategory == null) {
-                    onFabClick()
-                } else {
-                    toggleFavorite()
-                    presenter.moveMangaToCategory(defaultCategory, presenter.manga)
-                }
-            } else {
-                toggleFavorite()
-            }
-        }
-
-        // Set SwipeRefresh to refresh manga data.
-        swipe_refresh.setOnRefreshListener { fetchMangaFromSource() }
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.manga_info, menu)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        when (item.itemId) {
-            R.id.action_open_in_browser -> openInBrowser()
-            R.id.action_share -> shareManga()
-            R.id.action_add_to_home_screen -> addToHomeScreen()
-            else -> return super.onOptionsItemSelected(item)
-        }
-        return true
-    }
-
-    /**
-     * Check if manga is initialized.
-     * If true update view with manga information,
-     * if false fetch manga information
-     *
-     * @param manga  manga object containing information about manga.
-     * @param source the source of the manga.
-     */
-    fun onNextManga(manga: Manga, source: Source) {
-        if (manga.initialized) {
-            // Update view.
-            setMangaInfo(manga, source)
-        } else {
-            // Initialize manga.
-            fetchMangaFromSource()
-        }
-    }
-
-    /**
-     * Update the view with manga information.
-     *
-     * @param manga manga object containing information about manga.
-     * @param source the source of the manga.
-     */
-    private fun setMangaInfo(manga: Manga, source: Source?) {
-        // Update artist TextView.
-        manga_artist.text = manga.artist
-
-        // Update author TextView.
-        manga_author.text = manga.author
-
-        // If manga source is known update source TextView.
-        if (source != null) {
-            manga_source.text = source.toString()
-        }
-
-        // Update genres TextView.
-        manga_genres.text = manga.genre
-
-        // Update status TextView.
-        manga_status.setText(when (manga.status) {
-            SManga.ONGOING -> R.string.ongoing
-            SManga.COMPLETED -> R.string.completed
-            SManga.LICENSED -> R.string.licensed
-            else -> R.string.unknown
-        })
-
-        // Update description TextView.
-        manga_summary.text = manga.description
-
-        // Set the favorite drawable to the correct one.
-        setFavoriteDrawable(manga.favorite)
-
-        // Set cover if it wasn't already.
-        if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) {
-            Glide.with(this)
-                    .load(manga)
-                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
-                    .centerCrop()
-                    .into(manga_cover)
-
-            Glide.with(this)
-                    .load(manga)
-                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
-                    .centerCrop()
-                    .into(backdrop)
-        }
-    }
-
-    /**
-     * Update chapter count TextView.
-     *
-     * @param count number of chapters.
-     */
-    fun setChapterCount(count: Int) {
-        manga_chapters.text = count.toString()
-    }
-
-    /**
-     * Toggles the favorite status and asks for confirmation to delete downloaded chapters.
-     */
-    fun toggleFavorite() {
-        if (!isAdded) return
-
-        val isNowFavorite = presenter.toggleFavorite()
-        if (!isNowFavorite && presenter.hasDownloads()) {
-            view!!.snack(getString(R.string.delete_downloads_for_manga)) {
-                setAction(R.string.action_delete) {
-                    presenter.deleteDownloads()
-                }
-            }
-        }
-    }
-
-    /**
-     * Open the manga in browser.
-     */
-    fun openInBrowser() {
-        if (!isAdded) return
-
-        val source = presenter.source as? HttpSource ?: return
-        try {
-            val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString())
-            val intent = CustomTabsIntent.Builder()
-                    .setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
-                    .build()
-            intent.launchUrl(activity, url)
-        } catch (e: Exception) {
-            context.toast(e.message)
-        }
-    }
-
-    /**
-     * Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
-     */
-    private fun shareManga() {
-        if (!isAdded) return
-
-        val source = presenter.source as? HttpSource ?: return
-        try {
-            val url = source.mangaDetailsRequest(presenter.manga).url().toString()
-            val sharingIntent = Intent(Intent.ACTION_SEND).apply {
-                type = "text/plain"
-                putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text, presenter.manga.title, url))
-            }
-            startActivity(Intent.createChooser(sharingIntent, getString(R.string.action_share)))
-        } catch (e: Exception) {
-            context.toast(e.message)
-        }
-    }
-
-    /**
-     * Add the manga to the home screen
-     */
-    fun addToHomeScreen() {
-        if (!isAdded) return
-
-        val shortcutIntent = activity.intent
-        shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-                .putExtra(MangaActivity.FROM_LAUNCHER_EXTRA, true)
-
-        val addIntent = Intent()
-        addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent)
-                .action = "com.android.launcher.action.INSTALL_SHORTCUT"
-
-        //Set shortcut title
-        MaterialDialog.Builder(activity)
-                .title(R.string.shortcut_title)
-                .input("", presenter.manga.title, { md, text ->
-                    //Set shortcut title
-                    addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString())
-
-                    reshapeIconBitmap(addIntent,
-                            Glide.with(context).load(presenter.manga).asBitmap())
-                })
-                .negativeText(android.R.string.cancel)
-                .onNegative { materialDialog, dialogAction -> materialDialog.cancel() }
-                .show()
-    }
-
-    fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest<out Any>) {
-        val modes = intArrayOf(R.string.circular_icon,
-                R.string.rounded_icon,
-                R.string.square_icon,
-                R.string.star_icon)
-
-        fun BitmapRequestBuilder<out Any, Bitmap>.toIcon(): Bitmap {
-            return this.into(96, 96).get()
-        }
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.icon_shape)
-                .negativeText(android.R.string.cancel)
-                .items(modes.map { getString(it) })
-                .itemsCallback { dialog, view, i, charSequence ->
-                    Observable.fromCallable {
-                        // i = 0: Circular icon
-                        // i = 1: Rounded icon
-                        // i = 2: Square icon
-                        // i = 3: Star icon (because boredom)
-                        when (i) {
-                            0 -> request.transform(CropCircleTransformation(context)).toIcon()
-                            1 -> request.transform(RoundedCornersTransformation(context, 5, 0)).toIcon()
-                            2 -> request.transform(CropSquareTransformation(context)).toIcon()
-                            3 -> request.transform(CenterCrop(context), MaskTransformation(context, R.drawable.mask_star)).toIcon()
-                            else -> null
-                        }
-                    }.subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe({ if (it != null) createShortcut(addIntent, it) },
-                            { context.toast(R.string.icon_creation_fail) })
-                }.show()
-    }
-
-    fun createShortcut(addIntent: Intent, icon: Bitmap) {
-        //Send shortcut intent
-        addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon)
-        context.sendBroadcast(addIntent)
-        //Go to launcher to show this shiny new shortcut!
-        val startMain = Intent(Intent.ACTION_MAIN)
-        startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK
-        startActivity(startMain)
-    }
-
-    /**
-     * Update FAB with correct drawable.
-     *
-     * @param isFavorite determines if manga is favorite or not.
-     */
-    private fun setFavoriteDrawable(isFavorite: Boolean) {
-        // Set the Favorite drawable to the correct one.
-        // Border drawable if false, filled drawable if true.
-        fab_favorite.setImageResource(if (isFavorite)
-            R.drawable.ic_bookmark_white_24dp
-        else
-            R.drawable.ic_bookmark_border_white_24dp)
-    }
-
-    /**
-     * Start fetching manga information from source.
-     */
-    private fun fetchMangaFromSource() {
-        setRefreshing(true)
-        // Call presenter and start fetching manga information
-        presenter.fetchMangaFromSource()
-    }
-
-
-    /**
-     * Update swipe refresh to stop showing refresh in progress spinner.
-     */
-    fun onFetchMangaDone() {
-        setRefreshing(false)
-    }
-
-    /**
-     * Update swipe refresh to start showing refresh in progress spinner.
-     */
-    fun onFetchMangaError() {
-        setRefreshing(false)
-    }
-
-    /**
-     * Set swipe refresh status.
-     *
-     * @param value whether it should be refreshing or not.
-     */
-    private fun setRefreshing(value: Boolean) {
-        swipe_refresh.isRefreshing = value
-    }
-
-    /**
-     * Called when the fab is clicked.
-     */
-    private fun onFabClick() {
-        val categories = presenter.getCategories()
-
-        MaterialDialog.Builder(activity)
-                .title(R.string.action_move_category)
-                .items(categories.map { it.name })
-                .itemsCallbackMultiChoice(presenter.getMangaCategoryIds(presenter.manga)) { dialog, position, text ->
-                    if (position.contains(0) && position.count() > 1) {
-                        dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray())
-                        Toast.makeText(dialog.context, R.string.invalid_combination, Toast.LENGTH_SHORT).show()
-                    }
-
-                    true
-                }
-                .alwaysCallMultiChoiceCallback()
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { dialog, _ ->
-                    val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList()
-
-                    if(!selectedCategories.isEmpty()) {
-                        if(!presenter.manga.favorite) {
-                            toggleFavorite()
-                        }
-                        presenter.moveMangaToCategories(selectedCategories.filter { it.id != 0}, presenter.manga)
-                    } else {
-                        toggleFavorite()
-                    }
-                }
-                .build()
-                .show()
-    }
-
-}

+ 169 - 201
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt

@@ -1,201 +1,169 @@
-package eu.kanade.tachiyomi.ui.manga.info
-
-import android.os.Bundle
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.source.Source
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.manga.MangaEvent
-import eu.kanade.tachiyomi.util.SharedData
-import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.injectLazy
-
-/**
- * Presenter of MangaInfoFragment.
- * Contains information and data for fragment.
- * Observable updates should be called from here.
- */
-class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
-
-    /**
-     * Active manga.
-     */
-    lateinit var manga: Manga
-        private set
-
-    /**
-     * Source of the manga.
-     */
-    lateinit var source: Source
-        private set
-
-    /**
-     * Used to connect to database.
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Used to connect to different manga sources.
-     */
-    val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Used to connect to cache.
-     */
-    val coverCache: CoverCache by injectLazy()
-
-    private val downloadManager: DownloadManager by injectLazy()
-
-    /**
-     * Subscription to send the manga to the view.
-     */
-    private var viewMangaSubcription: Subscription? = null
-
-    /**
-     * Subscription to update the manga from the source.
-     */
-    private var fetchMangaSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
-        source = sourceManager.get(manga.source)!!
-        sendMangaToView()
-
-        // Update chapter count
-        SharedData.get(ChapterCountEvent::class.java)?.observable
-                ?.observeOn(AndroidSchedulers.mainThread())
-                ?.subscribeLatestCache(MangaInfoFragment::setChapterCount)
-
-        // Update favorite status
-        SharedData.get(MangaFavoriteEvent::class.java)?.let {
-            it.observable
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe { setFavorite(it) }
-                    .apply { add(this) }
-        }
-    }
-
-    /**
-     * Sends the active manga to the view.
-     */
-    fun sendMangaToView() {
-        viewMangaSubcription?.let { remove(it) }
-        viewMangaSubcription = Observable.just(manga)
-                .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
-    }
-
-    /**
-     * Fetch manga information from source.
-     */
-    fun fetchMangaFromSource() {
-        if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
-        fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
-                .map { networkManga ->
-                    manga.copyFrom(networkManga)
-                    manga.initialized = true
-                    db.insertManga(manga).executeAsBlocking()
-                    manga
-                }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .doOnNext { sendMangaToView() }
-                .subscribeFirst({ view, manga ->
-                    view.onFetchMangaDone()
-                }, { view, error ->
-                    view.onFetchMangaError()
-                })
-    }
-
-    /**
-     * Update favorite status of manga, (removes / adds) manga (to / from) library.
-     *
-     * @return the new status of the manga.
-     */
-    fun toggleFavorite(): Boolean {
-        manga.favorite = !manga.favorite
-        if (!manga.favorite) {
-            coverCache.deleteFromCache(manga.thumbnail_url)
-        }
-        db.insertManga(manga).executeAsBlocking()
-        sendMangaToView()
-        return manga.favorite
-    }
-
-    private fun setFavorite(favorite: Boolean) {
-        if (manga.favorite == favorite) {
-            return
-        }
-        toggleFavorite()
-    }
-
-    /**
-     * Returns true if the manga has any downloads.
-     */
-    fun hasDownloads(): Boolean {
-        return downloadManager.findMangaDir(source, manga) != null
-    }
-
-    /**
-     * Deletes all the downloads for the manga.
-     */
-    fun deleteDownloads() {
-        downloadManager.findMangaDir(source, manga)?.delete()
-    }
-
-    /**
-     * Get the default, and user categories.
-     *
-     * @return List of categories, default plus user categories
-     */
-    fun getCategories(): List<Category> {
-        return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
-    }
-
-    /**
-     * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
-     *
-     * @param manga the manga to get categories from.
-     * @return Array of category ids the manga is in, if none returns default id
-     */
-    fun getMangaCategoryIds(manga: Manga): Array<Int?> {
-        val categories = db.getCategoriesForManga(manga).executeAsBlocking()
-        if(categories.isEmpty()) {
-            return arrayListOf(Category.createDefault().id).toTypedArray()
-        }
-        return categories.map { it.id }.toTypedArray()
-    }
-
-    /**
-     * Move the given manga to categories.
-     *
-     * @param categories the selected categories.
-     * @param manga the manga to move.
-     */
-    fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
-        val mc = categories.map { MangaCategory.create(manga, it) }
-
-        db.setMangaCategories(mc, arrayListOf(manga))
-    }
-
-    /**
-     * Move the given manga to the category.
-     *
-     * @param category the selected category.
-     * @param manga the manga to move.
-     */
-    fun moveMangaToCategory(category: Category, manga: Manga) {
-        moveMangaToCategories(arrayListOf(category), manga)
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.info
+
+import android.os.Bundle
+import com.jakewharton.rxrelay.BehaviorRelay
+import com.jakewharton.rxrelay.PublishRelay
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaCategory
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * Presenter of MangaInfoFragment.
+ * Contains information and data for fragment.
+ * Observable updates should be called from here.
+ */
+class MangaInfoPresenter(
+        val manga: Manga,
+        val source: Source,
+        private val chapterCountRelay: BehaviorRelay<Int>,
+        private val mangaFavoriteRelay: PublishRelay<Boolean>,
+        private val db: DatabaseHelper = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get(),
+        private val coverCache: CoverCache = Injekt.get()
+) : BasePresenter<MangaInfoController>() {
+
+    /**
+     * Subscription to send the manga to the view.
+     */
+    private var viewMangaSubcription: Subscription? = null
+
+    /**
+     * Subscription to update the manga from the source.
+     */
+    private var fetchMangaSubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        sendMangaToView()
+
+        // Update chapter count
+        chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(MangaInfoController::setChapterCount)
+
+        // Update favorite status
+        mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
+                .subscribe { setFavorite(it) }
+                .apply { add(this) }
+    }
+
+    /**
+     * Sends the active manga to the view.
+     */
+    fun sendMangaToView() {
+        viewMangaSubcription?.let { remove(it) }
+        viewMangaSubcription = Observable.just(manga)
+                .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
+    }
+
+    /**
+     * Fetch manga information from source.
+     */
+    fun fetchMangaFromSource() {
+        if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
+        fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
+                .map { networkManga ->
+                    manga.copyFrom(networkManga)
+                    manga.initialized = true
+                    db.insertManga(manga).executeAsBlocking()
+                    manga
+                }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnNext { sendMangaToView() }
+                .subscribeFirst({ view, _ ->
+                    view.onFetchMangaDone()
+                }, { view, _ ->
+                    view.onFetchMangaError()
+                })
+    }
+
+    /**
+     * Update favorite status of manga, (removes / adds) manga (to / from) library.
+     *
+     * @return the new status of the manga.
+     */
+    fun toggleFavorite(): Boolean {
+        manga.favorite = !manga.favorite
+        if (!manga.favorite) {
+            coverCache.deleteFromCache(manga.thumbnail_url)
+        }
+        db.insertManga(manga).executeAsBlocking()
+        sendMangaToView()
+        return manga.favorite
+    }
+
+    private fun setFavorite(favorite: Boolean) {
+        if (manga.favorite == favorite) {
+            return
+        }
+        toggleFavorite()
+    }
+
+    /**
+     * Returns true if the manga has any downloads.
+     */
+    fun hasDownloads(): Boolean {
+        return downloadManager.findMangaDir(source, manga) != null
+    }
+
+    /**
+     * Deletes all the downloads for the manga.
+     */
+    fun deleteDownloads() {
+        downloadManager.findMangaDir(source, manga)?.delete()
+    }
+
+    /**
+     * Get the default, and user categories.
+     *
+     * @return List of categories, default plus user categories
+     */
+    fun getCategories(): List<Category> {
+        return db.getCategories().executeAsBlocking()
+    }
+
+    /**
+     * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
+     *
+     * @param manga the manga to get categories from.
+     * @return Array of category ids the manga is in, if none returns default id
+     */
+    fun getMangaCategoryIds(manga: Manga): Array<Int> {
+        val categories = db.getCategoriesForManga(manga).executeAsBlocking()
+        return categories.mapNotNull { it.id }.toTypedArray()
+    }
+
+    /**
+     * Move the given manga to categories.
+     *
+     * @param manga the manga to move.
+     * @param categories the selected categories.
+     */
+    fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
+        val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
+        db.setMangaCategories(mc, listOf(manga))
+    }
+
+    /**
+     * Move the given manga to the category.
+     *
+     * @param manga the manga to move.
+     * @param category the selected category, or null for default category.
+     */
+    fun moveMangaToCategory(manga: Manga, category: Category?) {
+        moveMangaToCategories(manga, listOfNotNull(category))
+    }
+
+}

+ 74 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt

@@ -0,0 +1,74 @@
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.widget.NumberPicker
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackChaptersDialog<T> : DialogController
+        where T : Controller, T : SetTrackChaptersDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+
+        val dialog = MaterialDialog.Builder(activity!!)
+                .title(R.string.chapters)
+                .customView(R.layout.dialog_track_chapters, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { dialog, _ ->
+                    val view = dialog.customView
+                    if (view != null) {
+                        // Remove focus to update selected number
+                        val np = view.findViewById(R.id.chapters_picker) as NumberPicker
+                        np.clearFocus()
+
+                        (targetController as? Listener)?.setChaptersRead(item, np.value)
+                    }
+                }
+                .build()
+
+        val view = dialog.customView
+        if (view != null) {
+            val np = view.findViewById(R.id.chapters_picker) as NumberPicker
+            // Set initial value
+            np.value = item.track?.last_chapter_read ?: 0
+            // Don't allow to go from 0 to 9999
+            np.wrapSelectorWheel = false
+        }
+
+        return dialog
+    }
+
+    interface Listener {
+        fun setChaptersRead(item: TrackItem, chaptersRead: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track"
+    }
+
+}

+ 80 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt

@@ -0,0 +1,80 @@
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.widget.NumberPicker
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackScoreDialog<T> : DialogController
+        where T : Controller, T : SetTrackScoreDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+
+        val dialog = MaterialDialog.Builder(activity!!)
+                .title(R.string.score)
+                .customView(R.layout.dialog_track_score, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { dialog, _ ->
+                    val view = dialog.customView
+                    if (view != null) {
+                        // Remove focus to update selected number
+                        val np = view.findViewById(R.id.score_picker) as NumberPicker
+                        np.clearFocus()
+
+                        (targetController as? Listener)?.setScore(item, np.value)
+                    }
+                }
+                .show()
+
+        val view = dialog.customView
+        if (view != null) {
+            val np = view.findViewById(R.id.score_picker) as NumberPicker
+            val scores = item.service.getScoreList().toTypedArray()
+            np.maxValue = scores.size - 1
+            np.displayedValues = scores
+
+            // Set initial value
+            val displayedScore = item.service.displayScore(item.track!!)
+            if (displayedScore != "-") {
+                val index = scores.indexOf(displayedScore)
+                np.value = if (index != -1) index else 0
+            }
+        }
+
+        return dialog
+    }
+
+    interface Listener {
+        fun setScore(item: TrackItem, score: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track"
+    }
+
+}

+ 58 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt

@@ -0,0 +1,58 @@
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SetTrackStatusDialog<T> : DialogController
+        where T : Controller, T : SetTrackStatusDialog.Listener {
+
+    private val item: TrackItem
+
+    constructor(target: T, item: TrackItem) : super(Bundle().apply {
+        putSerializable(KEY_ITEM_TRACK, item.track)
+    }) {
+        targetController = target
+        this.item = item
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
+        val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
+        item = TrackItem(track, service)
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val item = item
+        val statusList = item.service.getStatusList().orEmpty()
+        val statusString = statusList.mapNotNull { item.service.getStatus(it) }
+        val selectedIndex = statusList.indexOf(item.track?.status)
+
+        return MaterialDialog.Builder(activity!!)
+                .title(R.string.status)
+                .negativeText(android.R.string.cancel)
+                .items(statusString)
+                .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ ->
+                    (targetController as? Listener)?.setStatus(item, i)
+                    true
+                })
+                .build()
+    }
+
+    interface Listener {
+        fun setStatus(item: TrackItem, selection: Int)
+    }
+
+    private companion object {
+        const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track"
+    }
+
+}

+ 44 - 33
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt

@@ -1,33 +1,44 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.support.v7.widget.RecyclerView
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.inflate
-
-class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
-
-    var items = emptyList<TrackItem>()
-        set(value) {
-            if (field !== value) {
-                field = value
-                notifyDataSetChanged()
-            }
-        }
-
-    var onClickListener: (TrackItem) -> Unit = {}
-
-    override fun getItemCount(): Int {
-        return items.size
-    }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
-        val view = parent.inflate(R.layout.item_track)
-        return TrackHolder(view, fragment)
-    }
-
-    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
-        holder.onSetValues(items[position])
-    }
-
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.inflate
+
+class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
+
+    var items = emptyList<TrackItem>()
+        set(value) {
+            if (field !== value) {
+                field = value
+                notifyDataSetChanged()
+            }
+        }
+
+    val rowClickListener: OnRowClickListener = controller
+
+    fun getItem(index: Int): TrackItem? {
+        return items.getOrNull(index)
+    }
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
+        val view = parent.inflate(R.layout.item_track)
+        return TrackHolder(view, this)
+    }
+
+    override fun onBindViewHolder(holder: TrackHolder, position: Int) {
+        holder.bind(items[position])
+    }
+
+    interface OnRowClickListener {
+        fun onTitleClick(position: Int)
+        fun onStatusClick(position: Int)
+        fun onChaptersClick(position: Int)
+        fun onScoreClick(position: Int)
+    }
+
+}

+ 123 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt

@@ -0,0 +1,123 @@
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.fragment_track.view.*
+
+class TrackController : NucleusController<TrackPresenter>(),
+        TrackAdapter.OnRowClickListener,
+        SetTrackStatusDialog.Listener,
+        SetTrackChaptersDialog.Listener,
+        SetTrackScoreDialog.Listener {
+
+    private var adapter: TrackAdapter? = null
+
+    override fun createPresenter(): TrackPresenter {
+        return TrackPresenter((parentController as MangaController).manga!!)
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_track, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        adapter = TrackAdapter(this)
+        with(view) {
+            track_recycler.layoutManager = LinearLayoutManager(context)
+            track_recycler.adapter = adapter
+            swipe_refresh.isEnabled = false
+            swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+    }
+
+    fun onNextTrackings(trackings: List<TrackItem>) {
+        val atLeastOneLink = trackings.any { it.track != null }
+        adapter?.items = trackings
+        view?.swipe_refresh?.isEnabled = atLeastOneLink
+        (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
+    }
+
+    fun onSearchResults(results: List<Track>) {
+        getSearchDialog()?.onSearchResults(results)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    fun onSearchResultsError(error: Throwable) {
+        getSearchDialog()?.onSearchResultsError()
+    }
+
+    private fun getSearchDialog(): TrackSearchDialog? {
+        return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
+    }
+
+    fun onRefreshDone() {
+        view?.swipe_refresh?.isRefreshing = false
+    }
+
+    fun onRefreshError(error: Throwable) {
+        view?.swipe_refresh?.isRefreshing = false
+        activity?.toast(error.message)
+    }
+
+    override fun onTitleClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
+    }
+
+    override fun onStatusClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackStatusDialog(this, item).showDialog(router)
+    }
+
+    override fun onChaptersClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackChaptersDialog(this, item).showDialog(router)
+    }
+
+    override fun onScoreClick(position: Int) {
+        val item = adapter?.getItem(position) ?: return
+        if (item.track == null) return
+
+        SetTrackScoreDialog(this, item).showDialog(router)
+    }
+
+    override fun setStatus(item: TrackItem, selection: Int) {
+        presenter.setStatus(item, selection)
+        view?.swipe_refresh?.isRefreshing = true
+    }
+
+    override fun setScore(item: TrackItem, score: Int) {
+        presenter.setScore(item, score)
+        view?.swipe_refresh?.isRefreshing = true
+    }
+
+    override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
+        presenter.setLastChapterRead(item, chaptersRead)
+        view?.swipe_refresh?.isRefreshing = true
+    }
+
+    private companion object {
+        const val TAG_SEARCH_CONTROLLER = "track_search_controller"
+    }
+
+}

+ 0 - 173
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt

@@ -1,173 +0,0 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.os.Bundle
-import android.support.v7.widget.LinearLayoutManager
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.NumberPicker
-import com.afollestad.materialdialogs.MaterialDialog
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.fragment_track.*
-import nucleus.factory.RequiresPresenter
-
-@RequiresPresenter(TrackPresenter::class)
-class TrackFragment : BaseRxFragment<TrackPresenter>() {
-
-    companion object {
-        fun newInstance(): TrackFragment {
-            return TrackFragment()
-        }
-    }
-
-    private lateinit var adapter: TrackAdapter
-
-    private var dialog: TrackSearchDialog? = null
-
-    private val searchFragmentTag: String
-        get() = "search_fragment"
-
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
-        return inflater.inflate(R.layout.fragment_track, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        adapter = TrackAdapter(this)
-        recycler.layoutManager = LinearLayoutManager(context)
-        recycler.adapter = adapter
-        swipe_refresh.isEnabled = false
-        swipe_refresh.setOnRefreshListener { presenter.refresh() }
-    }
-
-    private fun findSearchFragmentIfNeeded() {
-        if (dialog == null) {
-            dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as? TrackSearchDialog
-        }
-    }
-
-    fun onNextTrackings(trackings: List<TrackItem>) {
-        adapter.items = trackings
-        swipe_refresh.isEnabled = trackings.any { it.track != null }
-        (activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null })
-    }
-
-    fun onSearchResults(results: List<Track>) {
-        if (!isResumed) return
-
-        findSearchFragmentIfNeeded()
-        dialog?.onSearchResults(results)
-    }
-
-    fun onSearchResultsError(error: Throwable) {
-        if (!isResumed) return
-
-        findSearchFragmentIfNeeded()
-        dialog?.onSearchResultsError()
-    }
-
-    fun onRefreshDone() {
-        swipe_refresh.isRefreshing = false
-    }
-
-    fun onRefreshError(error: Throwable) {
-        swipe_refresh.isRefreshing = false
-        context.toast(error.message)
-    }
-
-    fun onTitleClick(item: TrackItem) {
-        if (!isResumed) return
-
-        if (dialog == null) {
-            dialog = TrackSearchDialog.newInstance()
-        }
-
-        presenter.selectedService = item.service
-        dialog?.show(childFragmentManager, searchFragmentTag)
-    }
-
-    fun onStatusClick(item: TrackItem) {
-        if (!isResumed || item.track == null) return
-
-        val statusList = item.service.getStatusList().map { item.service.getStatus(it) }
-        val selectedIndex = item.service.getStatusList().indexOf(item.track.status)
-
-        MaterialDialog.Builder(context)
-                .title(R.string.status)
-                .items(statusList)
-                .itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence ->
-                    presenter.setStatus(item, i)
-                    swipe_refresh.isRefreshing = true
-                    true
-                })
-                .show()
-    }
-
-    fun onChaptersClick(item: TrackItem) {
-        if (!isResumed || item.track == null) return
-
-        val dialog = MaterialDialog.Builder(context)
-                .title(R.string.chapters)
-                .customView(R.layout.dialog_track_chapters, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { d, action ->
-                    val view = d.customView
-                    if (view != null) {
-                        val np = view.findViewById(R.id.chapters_picker) as NumberPicker
-                        np.clearFocus()
-                        presenter.setLastChapterRead(item, np.value)
-                        swipe_refresh.isRefreshing = true
-                    }
-                }
-                .show()
-
-        val view = dialog.customView
-        if (view != null) {
-            val np = view.findViewById(R.id.chapters_picker) as NumberPicker
-            // Set initial value
-            np.value = item.track.last_chapter_read
-            // Don't allow to go from 0 to 9999
-            np.wrapSelectorWheel = false
-        }
-    }
-
-    fun onScoreClick(item: TrackItem) {
-        if (!isResumed || item.track == null) return
-
-        val dialog = MaterialDialog.Builder(activity)
-                .title(R.string.score)
-                .customView(R.layout.dialog_track_score, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { d, action ->
-                    val view = d.customView
-                    if (view != null) {
-                        val np = view.findViewById(R.id.score_picker) as NumberPicker
-                        np.clearFocus()
-                        presenter.setScore(item, np.value)
-                        swipe_refresh.isRefreshing = true
-                    }
-                }
-                .show()
-
-        val view = dialog.customView
-        if (view != null) {
-            val np = view.findViewById(R.id.score_picker) as NumberPicker
-            val scores = item.service.getScoreList().toTypedArray()
-            np.maxValue = scores.size - 1
-            np.displayedValues = scores
-
-            // Set initial value
-            val displayedScore = item.service.displayScore(item.track)
-            if (displayedScore != "-") {
-                val index = scores.indexOf(displayedScore)
-                np.value = if (index != -1) index else 0
-            }
-        }
-    }
-
-}

+ 41 - 42
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt

@@ -1,42 +1,41 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.support.v7.widget.RecyclerView
-import android.view.View
-import eu.kanade.tachiyomi.R
-import kotlinx.android.synthetic.main.item_track.view.*
-
-class TrackHolder(private val view: View, private val fragment: TrackFragment)
-: RecyclerView.ViewHolder(view) {
-    
-    private lateinit var item: TrackItem
-
-    init {
-        view.title_container.setOnClickListener { fragment.onTitleClick(item) }
-        view.status_container.setOnClickListener { fragment.onStatusClick(item) }
-        view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
-        view.score_container.setOnClickListener { fragment.onScoreClick(item) }
-    }
-
-    @Suppress("DEPRECATION")
-    fun onSetValues(item: TrackItem) = with(view) {
-        [email protected] = item
-        val track = item.track
-        track_logo.setImageResource(item.service.getLogo())
-        logo.setBackgroundColor(item.service.getLogoColor())
-        if (track != null) {
-            track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
-            track_title.setAllCaps(false)
-            track_title.text = track.title
-            track_chapters.text = "${track.last_chapter_read}/" +
-                    if (track.total_chapters > 0) track.total_chapters else "-"
-            track_status.text = item.service.getStatus(track.status)
-            track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
-        } else {
-            track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
-            track_title.setText(R.string.action_edit)
-            track_chapters.text = ""
-            track_score.text = ""
-            track_status.text = ""
-        }
-    }
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.annotation.SuppressLint
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import eu.kanade.tachiyomi.R
+import kotlinx.android.synthetic.main.item_track.view.*
+
+class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) {
+    
+    init {
+        val listener = adapter.rowClickListener
+        view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
+        view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
+        view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
+        view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
+    }
+
+    @SuppressLint("SetTextI18n")
+    @Suppress("DEPRECATION")
+    fun bind(item: TrackItem) = with(itemView) {
+        val track = item.track
+        track_logo.setImageResource(item.service.getLogo())
+        logo.setBackgroundColor(item.service.getLogoColor())
+        if (track != null) {
+            track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
+            track_title.setAllCaps(false)
+            track_title.text = track.title
+            track_chapters.text = "${track.last_chapter_read}/" +
+                    if (track.total_chapters > 0) track.total_chapters else "-"
+            track_status.text = item.service.getStatus(track.status)
+            track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
+        } else {
+            track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
+            track_title.setText(R.string.action_edit)
+            track_chapters.text = ""
+            track_score.text = ""
+            track_status.text = ""
+        }
+    }
+}

+ 6 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt

@@ -1,8 +1,6 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackService
-
-class TrackItem(val track: Track?, val service: TrackService) {
-
-}
+package eu.kanade.tachiyomi.ui.manga.track
+
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackService
+
+data class TrackItem(val track: Track?, val service: TrackService)

+ 128 - 136
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt

@@ -1,137 +1,129 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.os.Bundle
-import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
-import eu.kanade.tachiyomi.ui.manga.MangaEvent
-import eu.kanade.tachiyomi.util.SharedData
-import eu.kanade.tachiyomi.util.toast
-import rx.Observable
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-import uy.kohesive.injekt.injectLazy
-
-class TrackPresenter : BasePresenter<TrackFragment>() {
-
-    private val db: DatabaseHelper by injectLazy()
-
-    private val trackManager: TrackManager by injectLazy()
-
-    lateinit var manga: Manga
-        private set
-
-    private var trackList: List<TrackItem> = emptyList()
-
-    private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
-
-    var selectedService: TrackService? = null
-
-    private var trackSubscription: Subscription? = null
-
-    private var searchSubscription: Subscription? = null
-
-    private var refreshSubscription: Subscription? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
-        fetchTrackings()
-    }
-
-    fun fetchTrackings() {
-        trackSubscription?.let { remove(it) }
-        trackSubscription = db.getTracks(manga)
-                .asRxObservable()
-                .map { tracks ->
-                    loggedServices.map { service ->
-                        TrackItem(tracks.find { it.sync_id == service.id }, service)
-                    }
-                }
-                .observeOn(AndroidSchedulers.mainThread())
-                .doOnNext { trackList = it }
-                .subscribeLatestCache(TrackFragment::onNextTrackings)
-    }
-
-    fun refresh() {
-        refreshSubscription?.let { remove(it) }
-        refreshSubscription = Observable.from(trackList)
-                .filter { it.track != null }
-                .concatMap { item ->
-                    item.service.refresh(item.track!!)
-                            .flatMap { db.insertTrack(it).asRxObservable() }
-                            .map { item }
-                            .onErrorReturn { item }
-                }
-                .toList()
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, result -> view.onRefreshDone() },
-                        TrackFragment::onRefreshError)
-    }
-
-    fun search(query: String) {
-        val service = selectedService ?: return
-
-        searchSubscription?.let { remove(it) }
-        searchSubscription = service.search(query)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(TrackFragment::onSearchResults,
-                        TrackFragment::onSearchResultsError)
-    }
-
-    fun registerTracking(item: Track?) {
-        val service = selectedService ?: return
-
-        if (item != null) {
-            item.manga_id = manga.id!!
-            add(service.bind(item)
-                    .flatMap { db.insertTrack(item).asRxObservable() }
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe({ },
-                            { error -> context.toast(error.message) }))
-        } else {
-            db.deleteTrackForManga(manga, service).executeAsBlocking()
-        }
-    }
-
-    private fun updateRemote(track: Track, service: TrackService) {
-        service.update(track)
-                .flatMap { db.insertTrack(track).asRxObservable() }
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, result -> view.onRefreshDone() },
-                        { view, error ->
-                            view.onRefreshError(error)
-
-                            // Restart on error to set old values
-                            fetchTrackings()
-                        })
-    }
-
-    fun setStatus(item: TrackItem, index: Int) {
-        val track = item.track!!
-        track.status = item.service.getStatusList()[index]
-        updateRemote(track, item.service)
-    }
-
-    fun setScore(item: TrackItem, index: Int) {
-        val track = item.track!!
-        track.score = item.service.indexToScore(index)
-        updateRemote(track, item.service)
-    }
-
-    fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
-        val track = item.track!!
-        track.last_chapter_read = chapterNumber
-        updateRemote(track, item.service)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.toast
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class TrackPresenter(
+        val manga: Manga,
+        preferences: PreferencesHelper = Injekt.get(),
+        private val db: DatabaseHelper = Injekt.get(),
+        private val trackManager: TrackManager = Injekt.get()
+) : BasePresenter<TrackController>() {
+
+    private val context = preferences.context
+
+    private var trackList: List<TrackItem> = emptyList()
+
+    private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
+
+    private var trackSubscription: Subscription? = null
+
+    private var searchSubscription: Subscription? = null
+
+    private var refreshSubscription: Subscription? = null
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        fetchTrackings()
+    }
+
+    fun fetchTrackings() {
+        trackSubscription?.let { remove(it) }
+        trackSubscription = db.getTracks(manga)
+                .asRxObservable()
+                .map { tracks ->
+                    loggedServices.map { service ->
+                        TrackItem(tracks.find { it.sync_id == service.id }, service)
+                    }
+                }
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnNext { trackList = it }
+                .subscribeLatestCache(TrackController::onNextTrackings)
+    }
+
+    fun refresh() {
+        refreshSubscription?.let { remove(it) }
+        refreshSubscription = Observable.from(trackList)
+                .filter { it.track != null }
+                .concatMap { item ->
+                    item.service.refresh(item.track!!)
+                            .flatMap { db.insertTrack(it).asRxObservable() }
+                            .map { item }
+                            .onErrorReturn { item }
+                }
+                .toList()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, result -> view.onRefreshDone() },
+                        TrackController::onRefreshError)
+    }
+
+    fun search(query: String, service: TrackService) {
+        searchSubscription?.let { remove(it) }
+        searchSubscription = service.search(query)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeLatestCache(TrackController::onSearchResults,
+                        TrackController::onSearchResultsError)
+    }
+
+    fun registerTracking(item: Track?, service: TrackService) {
+        if (item != null) {
+            item.manga_id = manga.id!!
+            add(service.bind(item)
+                    .flatMap { db.insertTrack(item).asRxObservable() }
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe({ },
+                            { error -> context.toast(error.message) }))
+        } else {
+            db.deleteTrackForManga(manga, service).executeAsBlocking()
+        }
+    }
+
+    private fun updateRemote(track: Track, service: TrackService) {
+        service.update(track)
+                .flatMap { db.insertTrack(track).asRxObservable() }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribeFirst({ view, result -> view.onRefreshDone() },
+                        { view, error ->
+                            view.onRefreshError(error)
+
+                            // Restart on error to set old values
+                            fetchTrackings()
+                        })
+    }
+
+    fun setStatus(item: TrackItem, index: Int) {
+        val track = item.track!!
+        track.status = item.service.getStatusList()[index]
+        updateRemote(track, item.service)
+    }
+
+    fun setScore(item: TrackItem, index: Int) {
+        val track = item.track!!
+        track.score = item.service.indexToScore(index)
+        updateRemote(track, item.service)
+    }
+
+    fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
+        val track = item.track!!
+        track.last_chapter_read = chapterNumber
+        updateRemote(track, item.service)
+    }
+
 }

+ 46 - 46
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt

@@ -1,47 +1,47 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.content.Context
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ArrayAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.util.inflate
-import kotlinx.android.synthetic.main.item_track_search.view.*
-import java.util.*
-
-class TrackSearchAdapter(context: Context)
-: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
-
-    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
-        var v = view
-        // Get the data item for this position
-        val track = getItem(position)
-        // Check if an existing view is being reused, otherwise inflate the view
-        val holder: TrackSearchHolder // view lookup cache stored in tag
-        if (v == null) {
-            v = parent.inflate(R.layout.item_track_search)
-            holder = TrackSearchHolder(v)
-            v.tag = holder
-        } else {
-            holder = v.tag as TrackSearchHolder
-        }
-        holder.onSetValues(track)
-        return v
-    }
-
-    fun setItems(syncs: List<Track>) {
-        setNotifyOnChange(false)
-        clear()
-        addAll(syncs)
-        notifyDataSetChanged()
-    }
-
-    class TrackSearchHolder(private val view: View) {
-
-        fun onSetValues(track: Track) {
-            view.track_search_title.text = track.title
-        }
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.util.inflate
+import kotlinx.android.synthetic.main.item_track_search.view.*
+import java.util.*
+
+class TrackSearchAdapter(context: Context)
+: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
+
+    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
+        var v = view
+        // Get the data item for this position
+        val track = getItem(position)
+        // Check if an existing view is being reused, otherwise inflate the view
+        val holder: TrackSearchHolder // view lookup cache stored in tag
+        if (v == null) {
+            v = parent.inflate(R.layout.item_track_search)
+            holder = TrackSearchHolder(v)
+            v.tag = holder
+        } else {
+            holder = v.tag as TrackSearchHolder
+        }
+        holder.onSetValues(track)
+        return v
+    }
+
+    fun setItems(syncs: List<Track>) {
+        setNotifyOnChange(false)
+        clear()
+        addAll(syncs)
+        notifyDataSetChanged()
+    }
+
+    class TrackSearchHolder(private val view: View) {
+
+        fun onSetValues(track: Track) {
+            view.track_search_title.text = track.title
+        }
+    }
+
 }

+ 143 - 118
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt

@@ -1,119 +1,144 @@
-package eu.kanade.tachiyomi.ui.manga.track
-
-import android.app.Dialog
-import android.os.Bundle
-import android.support.v4.app.DialogFragment
-import android.view.View
-import com.afollestad.materialdialogs.MaterialDialog
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.widget.SimpleTextWatcher
-import kotlinx.android.synthetic.main.dialog_track_search.view.*
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import java.util.concurrent.TimeUnit
-
-class TrackSearchDialog : DialogFragment() {
-
-    companion object {
-
-        fun newInstance(): TrackSearchDialog {
-            return TrackSearchDialog()
-        }
-    }
-
-    private lateinit var v: View
-
-    lateinit var adapter: TrackSearchAdapter
-        private set
-
-    private val queryRelay by lazy { PublishRelay.create<String>() }
-
-    private var searchDebounceSubscription: Subscription? = null
-
-    private var selectedItem: Track? = null
-
-    val presenter: TrackPresenter
-        get() = (parentFragment as TrackFragment).presenter
-
-    override fun onCreateDialog(savedState: Bundle?): Dialog {
-        val dialog = MaterialDialog.Builder(context)
-                .customView(R.layout.dialog_track_search, false)
-                .positiveText(android.R.string.ok)
-                .negativeText(android.R.string.cancel)
-                .onPositive { dialog1, which -> onPositiveButtonClick() }
-                .build()
-
-        onViewCreated(dialog.view, savedState)
-
-        return dialog
-    }
-
-    override fun onViewCreated(view: View, savedState: Bundle?) {
-        v = view
-
-        // Create adapter
-        adapter = TrackSearchAdapter(context)
-        view.track_search_list.adapter = adapter
-
-        // Set listeners
-        selectedItem = null
-        view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
-            selectedItem = adapter.getItem(position)
-        }
-
-        // Do an initial search based on the manga's title
-        if (savedState == null) {
-            val title = presenter.manga.title
-            view.track_search.append(title)
-            search(title)
-        }
-
-        view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
-            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                queryRelay.call(s.toString())
-            }
-        })
-    }
-
-    override fun onResume() {
-        super.onResume()
-
-        // Listen to text changes
-        searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
-                .observeOn(AndroidSchedulers.mainThread())
-                .filter { it.isNotBlank() }
-                .subscribe { search(it) }
-    }
-
-    override fun onPause() {
-        searchDebounceSubscription?.unsubscribe()
-        super.onPause()
-    }
-
-    private fun search(query: String) {
-        v.progress.visibility = View.VISIBLE
-        v.track_search_list.visibility = View.GONE
-
-        presenter.search(query)
-    }
-
-    fun onSearchResults(results: List<Track>) {
-        selectedItem = null
-        v.progress.visibility = View.GONE
-        v.track_search_list.visibility = View.VISIBLE
-        adapter.setItems(results)
-    }
-
-    fun onSearchResultsError() {
-        v.progress.visibility = View.VISIBLE
-        v.track_search_list.visibility = View.GONE
-        adapter.setItems(emptyList())
-    }
-
-    private fun onPositiveButtonClick() {
-        presenter.registerTracking(selectedItem)
-    }
-
+package eu.kanade.tachiyomi.ui.manga.track
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.View
+import com.afollestad.materialdialogs.MaterialDialog
+import com.jakewharton.rxbinding.widget.itemClicks
+import com.jakewharton.rxbinding.widget.textChanges
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.util.plusAssign
+import kotlinx.android.synthetic.main.dialog_track_search.view.*
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.subscriptions.CompositeSubscription
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.concurrent.TimeUnit
+
+class TrackSearchDialog : DialogController {
+
+    private var dialogView: View? = null
+
+    private var adapter: TrackSearchAdapter? = null
+
+    private var selectedItem: Track? = null
+
+    private val service: TrackService
+
+    private var subscriptions = CompositeSubscription()
+
+    private var searchTextSubscription: Subscription? = null
+
+    private val trackController
+        get() = targetController as TrackController
+
+    constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
+        putInt(KEY_SERVICE, service.id)
+    }) {
+        targetController = target
+        this.service = service
+    }
+
+    @Suppress("unused")
+    constructor(bundle: Bundle) : super(bundle) {
+        service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
+    }
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        val dialog = MaterialDialog.Builder(activity!!)
+                .customView(R.layout.dialog_track_search, false)
+                .positiveText(android.R.string.ok)
+                .negativeText(android.R.string.cancel)
+                .onPositive { _, _ -> onPositiveButtonClick() }
+                .build()
+
+        if (subscriptions.isUnsubscribed) {
+            subscriptions = CompositeSubscription()
+        }
+
+        dialogView = dialog.view
+        onViewCreated(dialog.view, savedState)
+
+        return dialog
+    }
+
+    fun onViewCreated(view: View, savedState: Bundle?) {
+        // Create adapter
+        val adapter = TrackSearchAdapter(view.context)
+        this.adapter = adapter
+        view.track_search_list.adapter = adapter
+
+        // Set listeners
+        selectedItem = null
+
+        subscriptions += view.track_search_list.itemClicks().subscribe { position ->
+            selectedItem = adapter.getItem(position)
+        }
+
+        // Do an initial search based on the manga's title
+        if (savedState == null) {
+            val title = trackController.presenter.manga.title
+            view.track_search.append(title)
+            search(title)
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        subscriptions.unsubscribe()
+        dialogView = null
+        adapter = null
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        searchTextSubscription = dialogView!!.track_search.textChanges()
+                .skip(1)
+                .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
+                .map { it.toString() }
+                .filter(String::isNotBlank)
+                .subscribe { search(it) }
+    }
+
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+        searchTextSubscription?.unsubscribe()
+    }
+
+    private fun search(query: String) {
+        val view = dialogView ?: return
+        view.progress.visibility = View.VISIBLE
+        view.track_search_list.visibility = View.GONE
+
+        trackController.presenter.search(query, service)
+    }
+
+    fun onSearchResults(results: List<Track>) {
+        selectedItem = null
+        val view = dialogView ?: return
+        view.progress.visibility = View.GONE
+        view.track_search_list.visibility = View.VISIBLE
+        adapter?.setItems(results)
+    }
+
+    fun onSearchResultsError() {
+        val view = dialogView ?: return
+        view.progress.visibility = View.VISIBLE
+        view.track_search_list.visibility = View.GONE
+        adapter?.setItems(emptyList())
+    }
+
+    private fun onPositiveButtonClick() {
+        trackController.presenter.registerTracking(selectedItem, service)
+    }
+
+    private companion object {
+        const val KEY_SERVICE = "service_id"
+    }
+
 }

+ 13 - 36
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -28,7 +28,8 @@ import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import timber.log.Timber
-import uy.kohesive.injekt.injectLazy
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import java.io.File
 import java.net.URLConnection
 import java.util.*
@@ -36,41 +37,17 @@ import java.util.*
 /**
  * Presenter of [ReaderActivity].
  */
-class ReaderPresenter : BasePresenter<ReaderActivity>() {
-    /**
-     * Preferences.
-     */
-    val prefs: PreferencesHelper by injectLazy()
-
-    /**
-     * Database.
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Download manager.
-     */
-    val downloadManager: DownloadManager by injectLazy()
-
-    /**
-     * Tracking manager.
-     */
-    val trackManager: TrackManager by injectLazy()
-
-    /**
-     * Source manager.
-     */
-    val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Chapter cache.
-     */
-    val chapterCache: ChapterCache by injectLazy()
-
-    /**
-     * Cover cache.
-     */
-    val coverCache: CoverCache by injectLazy()
+class ReaderPresenter(
+        val prefs: PreferencesHelper = Injekt.get(),
+        val db: DatabaseHelper = Injekt.get(),
+        val downloadManager: DownloadManager = Injekt.get(),
+        val trackManager: TrackManager = Injekt.get(),
+        val sourceManager: SourceManager = Injekt.get(),
+        val chapterCache: ChapterCache = Injekt.get(),
+        val coverCache: CoverCache = Injekt.get()
+) : BasePresenter<ReaderActivity>() {
+
+    private val context = prefs.context
 
     /**
      * Manga being read.

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt

@@ -1,9 +1,9 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.base
 
+import android.support.v4.app.Fragment
 import com.davemorrissey.labs.subscaleview.decoder.*
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import java.util.*
@@ -12,7 +12,7 @@ import java.util.*
  * Base reader containing the common data that can be used by its implementations. It does not
  * contain any UI related action.
  */
-abstract class BaseReader : BaseFragment() {
+abstract class BaseReader : Fragment() {
 
     companion object {
         /**

+ 34 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt

@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.ui.recent_updates
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class ConfirmDeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T : ConfirmDeleteChaptersDialog.Listener {
+
+    private var chaptersToDelete = emptyList<RecentChapterItem>()
+
+    constructor(target: T, chaptersToDelete: List<RecentChapterItem>) : this() {
+        this.chaptersToDelete = chaptersToDelete
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .content(R.string.confirm_delete_chapters)
+                .positiveText(android.R.string.yes)
+                .negativeText(android.R.string.no)
+                .onPositive { _, _ ->
+                    (targetController as? Listener)?.deleteChapters(chaptersToDelete)
+                }
+                .build()
+    }
+
+    interface Listener {
+        fun deleteChapters(chaptersToDelete: List<RecentChapterItem>)
+    }
+}

+ 27 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt

@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.ui.recent_updates
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Router
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+
+class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
+
+    companion object {
+        const val TAG = "deleting_dialog"
+    }
+
+    override fun onCreateDialog(savedState: Bundle?): Dialog {
+        return MaterialDialog.Builder(activity!!)
+                .progress(true, 0)
+                .content(R.string.deleting)
+                .build()
+    }
+
+    override fun showDialog(router: Router) {
+        showDialog(router, TAG)
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt

@@ -115,7 +115,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
 
         // Set a listener so we are notified if a menu item is clicked
         popup.setOnMenuItemClickListener { menuItem ->
-            with(adapter.fragment) {
+            with(adapter.controller) {
                 when (menuItem.itemId) {
                     R.id.action_download -> downloadChapter(item)
                     R.id.action_delete -> deleteChapter(item)

+ 11 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt

@@ -27,11 +27,19 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem
         return R.layout.item_recent_chapters
     }
 
-    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): RecentChapterHolder {
-        return RecentChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as RecentChaptersAdapter)
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
+                                  parent: ViewGroup): RecentChapterHolder {
+
+        val view = inflater.inflate(layoutRes, parent, false)
+        return RecentChapterHolder(view , adapter as RecentChaptersAdapter)
     }
 
-    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: RecentChapterHolder, position: Int, payloads: List<Any?>?) {
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: RecentChapterHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
         holder.bind(this)
     }
 

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt

@@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.recent_updates
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 
-class RecentChaptersAdapter(val fragment: RecentChaptersFragment) :
-        FlexibleAdapter<IFlexible<*>>(null, fragment, true) {
+class RecentChaptersAdapter(val controller: RecentChaptersController) :
+        FlexibleAdapter<IFlexible<*>>(null, controller, true) {
 
     init {
         setDisplayHeadersAtStartUp(true)

+ 322 - 339
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt

@@ -1,340 +1,323 @@
-package eu.kanade.tachiyomi.ui.recent_updates
-
-import android.os.Bundle
-import android.support.v4.app.DialogFragment
-import android.support.v7.app.AppCompatActivity
-import android.support.v7.view.ActionMode
-import android.support.v7.widget.DividerItemDecoration
-import android.support.v7.widget.LinearLayoutManager
-import android.support.v7.widget.RecyclerView
-import android.view.*
-import com.afollestad.materialdialogs.MaterialDialog
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.download.model.Download
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-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.toast
-import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
-import kotlinx.android.synthetic.main.activity_main.*
-import kotlinx.android.synthetic.main.fragment_recent_chapters.*
-import nucleus.factory.RequiresPresenter
-import timber.log.Timber
-
-/**
- * Fragment that shows recent chapters.
- * Uses [R.layout.fragment_recent_chapters].
- * UI related actions should be called from here.
- */
-@RequiresPresenter(RecentChaptersPresenter::class)
-class RecentChaptersFragment:
-        BaseRxFragment<RecentChaptersPresenter>(),
-        ActionMode.Callback,
-        FlexibleAdapter.OnItemClickListener,
-        FlexibleAdapter.OnItemLongClickListener{
-
-    companion object {
-        /**
-         * Create new RecentChaptersFragment.
-         * @return a new instance of [RecentChaptersFragment].
-         */
-        fun newInstance(): RecentChaptersFragment {
-            return RecentChaptersFragment()
-        }
-    }
-
-    /**
-     * Action mode for multiple selection.
-     */
-    private var actionMode: ActionMode? = null
-
-    /**
-     * Adapter containing the recent chapters.
-     */
-    lateinit var adapter: RecentChaptersAdapter
-        private set
-
-    /**
-     * Called when view gets created
-     * @param inflater layout inflater
-     * @param container view group
-     * @param savedState status of saved state
-     */
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
-        // Inflate view
-        return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
-    }
-
-    /**
-     * Called when view is created
-     * @param view created view
-     * @param savedState status of saved sate
-     */
-    override fun onViewCreated(view: View, savedState: Bundle?) {
-        // Init RecyclerView and adapter
-        recycler.layoutManager = LinearLayoutManager(activity)
-        recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
-        recycler.setHasFixedSize(true)
-        adapter = RecentChaptersAdapter(this)
-        recycler.adapter = adapter
-
-        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
-            override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
-                // Disable swipe refresh when view is not at the top
-                val firstPos = (recycler.layoutManager as LinearLayoutManager)
-                        .findFirstCompletelyVisibleItemPosition()
-                swipe_refresh.isEnabled = firstPos == 0
-            }
-        })
-
-        swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
-        swipe_refresh.setOnRefreshListener {
-            if (!LibraryUpdateService.isRunning(activity)) {
-                LibraryUpdateService.start(activity)
-                context.toast(R.string.action_update_library)
-            }
-            // It can be a very long operation, so we disable swipe refresh and show a toast.
-            swipe_refresh.isRefreshing = false
-        }
-
-        // Update toolbar text
-        setToolbarTitle(R.string.label_recent_updates)
-
-        // Disable toolbar elevation, it looks better with sticky headers.
-        activity.appbar.disableElevation()
-    }
-
-    override fun onDestroyView() {
-        // Restore toolbar elevation.
-        activity.appbar.enableElevation()
-        super.onDestroyView()
-    }
-
-    /**
-     * Returns selected chapters
-     * @return list of selected chapters
-     */
-    fun getSelectedChapters(): List<RecentChapterItem> {
-        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
-    }
-
-    /**
-     * Called when item in list is clicked
-     * @param position position of clicked item
-     */
-    override fun onItemClick(position: Int): Boolean {
-        // Get item from position
-        val item = adapter.getItem(position) as? RecentChapterItem ?: return false
-        if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
-            toggleSelection(position)
-            return true
-        } else {
-            openChapter(item)
-            return false
-        }
-    }
-
-    /**
-     * Called when item in list is long clicked
-     * @param position position of clicked item
-     */
-    override fun onItemLongClick(position: Int) {
-        if (actionMode == null)
-            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
-
-        toggleSelection(position)
-    }
-
-    /**
-     * Called to toggle selection
-     * @param position position of selected item
-     */
-    private fun toggleSelection(position: Int) {
-        adapter.toggleSelection(position)
-
-        val count = adapter.selectedItemCount
-        if (count == 0) {
-            actionMode?.finish()
-        } else {
-            actionMode?.title = getString(R.string.label_selected, count)
-        }
-    }
-
-    /**
-     * Open chapter in reader
-     * @param chapter selected chapter
-     */
-    private fun openChapter(item: RecentChapterItem) {
-        val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
-        startActivity(intent)
-    }
-
-    /**
-     * Download selected items
-     * @param chapters list of selected [RecentChapter]s
-     */
-    fun downloadChapters(chapters: List<RecentChapterItem>) {
-        destroyActionModeIfNeeded()
-        presenter.downloadChapters(chapters)
-    }
-
-    /**
-     * Populate adapter with chapters
-     * @param chapters list of [Any]
-     */
-    fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
-        (activity as MainActivity).updateEmptyView(chapters.isEmpty(),
-                R.string.information_no_recent, R.drawable.ic_update_black_128dp)
-
-        destroyActionModeIfNeeded()
-        adapter.updateDataSet(chapters.toMutableList())
-    }
-
-    /**
-     * Update download status of chapter
-     * @param download [Download] object containing download progress.
-     */
-    fun onChapterStatusChange(download: Download) {
-        getHolder(download)?.notifyStatus(download.status)
-    }
-
-    /**
-     * Returns holder belonging to chapter
-     * @param download [Download] object containing download progress.
-     */
-    private fun getHolder(download: Download): RecentChapterHolder? {
-        return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
-    }
-
-    /**
-     * Mark chapter as read
-     * @param chapters list of chapters
-     */
-    fun markAsRead(chapters: List<RecentChapterItem>) {
-        presenter.markChapterRead(chapters, true)
-        if (presenter.preferences.removeAfterMarkedAsRead()) {
-            deleteChapters(chapters)
-        }
-    }
-
-    /**
-     * Delete selected chapters
-     * @param chapters list of [RecentChapter] objects
-     */
-    fun deleteChapters(chapters: List<RecentChapterItem>) {
-        destroyActionModeIfNeeded()
-        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
-        presenter.deleteChapters(chapters)
-    }
-
-    /**
-     * Destory [ActionMode] if it's shown
-     */
-    fun destroyActionModeIfNeeded() {
-        actionMode?.finish()
-    }
-
-    /**
-     * Mark chapter as unread
-     * @param chapters list of selected [RecentChapter]
-     */
-    fun markAsUnread(chapters: List<RecentChapterItem>) {
-        presenter.markChapterRead(chapters, false)
-    }
-
-    /**
-     * Start downloading chapter
-     * @param chapter selected chapter with manga
-     */
-    fun downloadChapter(chapter: RecentChapterItem) {
-        presenter.downloadChapters(listOf(chapter))
-    }
-
-    /**
-     * Start deleting chapter
-     * @param chapter selected chapter with manga
-     */
-    fun deleteChapter(chapter: RecentChapterItem) {
-        DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
-        presenter.deleteChapters(listOf(chapter))
-    }
-
-    /**
-     * 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)
-    }
-
-    /**
-     * Called to dismiss deleting dialog
-     */
-    fun dismissDeletingDialog() {
-        (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)
-                ?.dismissAllowingStateLoss()
-    }
-
-    /**
-     * Called when ActionMode item clicked
-     * @param mode the ActionMode object
-     * @param item item from ActionMode.
-     */
-    override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
-        if (!isAdded) return true
-
-        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
-    }
-
-    override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
-        return false
-    }
-
-    /**
-     * Called when ActionMode destroyed
-     * @param mode the ActionMode object
-     */
-    override fun onDestroyActionMode(mode: ActionMode?) {
-        adapter.mode = FlexibleAdapter.MODE_IDLE
-        adapter.clearSelection()
-        actionMode = null
-    }
-
+package eu.kanade.tachiyomi.ui.recent_updates
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.view.ActionMode
+import android.support.v7.widget.DividerItemDecoration
+import android.support.v7.widget.LinearLayoutManager
+import android.view.*
+import com.jakewharton.rxbinding.support.v4.widget.refreshes
+import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.fragment_recent_chapters.view.*
+import timber.log.Timber
+
+/**
+ * Fragment that shows recent chapters.
+ * Uses [R.layout.fragment_recent_chapters].
+ * UI related actions should be called from here.
+ */
+class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
+        NoToolbarElevationController,
+        ActionMode.Callback,
+        FlexibleAdapter.OnItemClickListener,
+        FlexibleAdapter.OnItemLongClickListener,
+        FlexibleAdapter.OnUpdateListener,
+        ConfirmDeleteChaptersDialog.Listener {
+
+    /**
+     * Action mode for multiple selection.
+     */
+    private var actionMode: ActionMode? = null
+
+    /**
+     * Adapter containing the recent chapters.
+     */
+    var adapter: RecentChaptersAdapter? = null
+        private set
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_recent_updates)
+    }
+
+    override fun createPresenter(): RecentChaptersPresenter {
+        return RecentChaptersPresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
+    }
+
+    /**
+     * Called when view is created
+     * @param view created view
+     * @param savedViewState status of saved sate
+     */
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        with(view) {
+            // Init RecyclerView and adapter
+            val layoutManager = LinearLayoutManager(context)
+            recycler.layoutManager = layoutManager
+            recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
+            recycler.setHasFixedSize(true)
+            adapter = RecentChaptersAdapter(this@RecentChaptersController)
+            recycler.adapter = adapter
+
+            recycler.scrollStateChanges().subscribeUntilDestroy {
+                // Disable swipe refresh when view is not at the top
+                val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
+                swipe_refresh.isEnabled = firstPos == 0
+            }
+
+            swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
+            swipe_refresh.refreshes().subscribeUntilDestroy {
+                if (!LibraryUpdateService.isRunning(context)) {
+                    LibraryUpdateService.start(context)
+                    context.toast(R.string.action_update_library)
+                }
+                // It can be a very long operation, so we disable swipe refresh and show a toast.
+                swipe_refresh.isRefreshing = false
+            }
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+        actionMode = null
+    }
+
+    /**
+     * Returns selected chapters
+     * @return list of selected chapters
+     */
+    fun getSelectedChapters(): List<RecentChapterItem> {
+        val adapter = adapter ?: return emptyList()
+        return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
+    }
+
+    /**
+     * Called when item in list is clicked
+     * @param position position of clicked item
+     */
+    override fun onItemClick(position: Int): Boolean {
+        val adapter = adapter ?: return false
+
+        // Get item from position
+        val item = adapter.getItem(position) as? RecentChapterItem ?: return false
+        if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
+            toggleSelection(position)
+            return true
+        } else {
+            openChapter(item)
+            return false
+        }
+    }
+
+    /**
+     * Called when item in list is long clicked
+     * @param position position of clicked item
+     */
+    override fun onItemLongClick(position: Int) {
+        if (actionMode == null)
+            actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
+
+        toggleSelection(position)
+    }
+
+    /**
+     * Called to toggle selection
+     * @param position position of selected item
+     */
+    private fun toggleSelection(position: Int) {
+        val adapter = adapter ?: return
+        adapter.toggleSelection(position)
+        actionMode?.invalidate()
+    }
+
+    /**
+     * Open chapter in reader
+     * @param chapter selected chapter
+     */
+    private fun openChapter(item: RecentChapterItem) {
+        val activity = activity ?: return
+        val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
+        startActivity(intent)
+    }
+
+    /**
+     * Download selected items
+     * @param chapters list of selected [RecentChapter]s
+     */
+    fun downloadChapters(chapters: List<RecentChapterItem>) {
+        destroyActionModeIfNeeded()
+        presenter.downloadChapters(chapters)
+    }
+
+    /**
+     * Populate adapter with chapters
+     * @param chapters list of [Any]
+     */
+    fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
+        destroyActionModeIfNeeded()
+        adapter?.updateDataSet(chapters.toMutableList())
+    }
+
+    override fun onUpdateEmptyView(size: Int) {
+        val emptyView = view?.empty_view ?: return
+        if (size > 0) {
+            emptyView.hide()
+        } else {
+            emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
+        }
+    }
+
+    /**
+     * Update download status of chapter
+     * @param download [Download] object containing download progress.
+     */
+    fun onChapterStatusChange(download: Download) {
+        getHolder(download)?.notifyStatus(download.status)
+    }
+
+    /**
+     * Returns holder belonging to chapter
+     * @param download [Download] object containing download progress.
+     */
+    private fun getHolder(download: Download): RecentChapterHolder? {
+        return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
+    }
+
+    /**
+     * Mark chapter as read
+     * @param chapters list of chapters
+     */
+    fun markAsRead(chapters: List<RecentChapterItem>) {
+        presenter.markChapterRead(chapters, true)
+        if (presenter.preferences.removeAfterMarkedAsRead()) {
+            deleteChapters(chapters)
+        }
+    }
+
+    override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) {
+        destroyActionModeIfNeeded()
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(chaptersToDelete)
+    }
+
+    /**
+     * Destory [ActionMode] if it's shown
+     */
+    fun destroyActionModeIfNeeded() {
+        actionMode?.finish()
+    }
+
+    /**
+     * Mark chapter as unread
+     * @param chapters list of selected [RecentChapter]
+     */
+    fun markAsUnread(chapters: List<RecentChapterItem>) {
+        presenter.markChapterRead(chapters, false)
+    }
+
+    /**
+     * Start downloading chapter
+     * @param chapter selected chapter with manga
+     */
+    fun downloadChapter(chapter: RecentChapterItem) {
+        presenter.downloadChapters(listOf(chapter))
+    }
+
+    /**
+     * Start deleting chapter
+     * @param chapter selected chapter with manga
+     */
+    fun deleteChapter(chapter: RecentChapterItem) {
+        DeletingChaptersDialog().showDialog(router)
+        presenter.deleteChapters(listOf(chapter))
+    }
+
+    /**
+     * 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)
+    }
+
+    /**
+     * Called to dismiss deleting dialog
+     */
+    fun dismissDeletingDialog() {
+        router.popControllerWithTag(DeletingChaptersDialog.TAG)
+    }
+
+    /**
+     * 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
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
+        val count = adapter?.selectedItemCount ?: 0
+        if (count == 0) {
+            // Destroy action mode if there are no items selected.
+            destroyActionModeIfNeeded()
+        } else {
+            mode.title = resources?.getString(R.string.label_selected, count)
+        }
+        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 -> ConfirmDeleteChaptersDialog(this, getSelectedChapters())
+                    .showDialog(router)
+            else -> return false
+        }
+        return true
+    }
+
+    /**
+     * Called when ActionMode destroyed
+     * @param mode the ActionMode object
+     */
+    override fun onDestroyActionMode(mode: ActionMode?) {
+        adapter?.mode = FlexibleAdapter.MODE_IDLE
+        adapter?.clearSelection()
+        actionMode = null
+    }
+
 }

+ 14 - 25
app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt

@@ -14,29 +14,18 @@ import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import timber.log.Timber
-import uy.kohesive.injekt.injectLazy
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import java.util.*
 
-class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
-    /**
-     * Used to connect to database
-     */
-    val db: DatabaseHelper by injectLazy()
-
-    /**
-     * Used to get settings
-     */
-    val preferences: PreferencesHelper by injectLazy()
+class RecentChaptersPresenter(
+        val preferences: PreferencesHelper = Injekt.get(),
+        private val db: DatabaseHelper = Injekt.get(),
+        private val downloadManager: DownloadManager = Injekt.get(),
+        private val sourceManager: SourceManager = Injekt.get()
+) : BasePresenter<RecentChaptersController>() {
 
-    /**
-     * Used to get information from download manager
-     */
-    val downloadManager: DownloadManager by injectLazy()
-
-    /**
-     * Used to get source from source id
-     */
-    val sourceManager: SourceManager by injectLazy()
+    private val context = preferences.context
 
     /**
      * List containing chapter and manga information
@@ -48,11 +37,11 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
 
         getRecentChaptersObservable()
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribeLatestCache(RecentChaptersFragment::onNextRecentChapters)
+                .subscribeLatestCache(RecentChaptersController::onNextRecentChapters)
 
         getChapterStatusObservable()
-                .subscribeLatestCache(RecentChaptersFragment::onChapterStatusChange,
-                        { view, error -> Timber.e(error) })
+                .subscribeLatestCache(RecentChaptersController::onChapterStatusChange,
+                        { _, error -> Timber.e(error) })
     }
 
     /**
@@ -207,9 +196,9 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
                 .toList()
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, result ->
+                .subscribeFirst({ view, _ ->
                     view.onChaptersDeleted()
-                }, RecentChaptersFragment::onChaptersDeletedError)
+                }, RecentChaptersController::onChaptersDeletedError)
     }
 
     /**

+ 24 - 33
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt

@@ -1,57 +1,48 @@
 package eu.kanade.tachiyomi.ui.recently_read
 
-import android.view.ViewGroup
-import eu.davidea.flexibleadapter4.FlexibleAdapter
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.util.inflate
 import uy.kohesive.injekt.injectLazy
+import java.text.DateFormat
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
 
 /**
  * Adapter of RecentlyReadHolder.
  * Connection between Fragment and Holder
  * Holder updates should be called from here.
  *
- * @param fragment a RecentlyReadFragment object
+ * @param controller a RecentlyReadController object
  * @constructor creates an instance of the adapter.
  */
-class RecentlyReadAdapter(val fragment: RecentlyReadFragment)
-: FlexibleAdapter<RecentlyReadHolder, MangaChapterHistory>() {
+class RecentlyReadAdapter(controller: RecentlyReadController)
+: FlexibleAdapter<RecentlyReadItem>(null, controller, true) {
 
     val sourceManager by injectLazy<SourceManager>()
 
-    /**
-     * Called when ViewHolder is created
-     * @param parent parent View
-     * @param viewType int containing viewType
-     */
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentlyReadHolder {
-        val view = parent.inflate(R.layout.item_recently_read)
-        return RecentlyReadHolder(view, this)
-    }
+    val resumeClickListener: OnResumeClickListener = controller
 
-    /**
-     * Called when ViewHolder is bind
-     * @param holder bind holder
-     * @param position position of holder
-     */
-    override fun onBindViewHolder(holder: RecentlyReadHolder, position: Int) {
-        val item = getItem(position)
-        holder.onSetValues(item)
-    }
+    val removeClickListener: OnRemoveClickListener = controller
+
+    val coverClickListener: OnCoverClickListener = controller
 
     /**
-     * Update items
-     * @param items items
+     * DecimalFormat used to display correct chapter number
      */
-    fun setItems(items: List<MangaChapterHistory>) {
-        mItems = items
-        notifyDataSetChanged()
+    val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
+            .apply { decimalSeparator = '.' })
+
+    val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
+
+    interface OnResumeClickListener {
+        fun onResumeClick(position: Int)
     }
 
-    override fun updateDataSet(param: String?) {
-        // Empty function
+    interface OnRemoveClickListener {
+        fun onRemoveClick(position: Int)
     }
 
+    interface OnCoverClickListener {
+        fun onCoverClick(position: Int)
+    }
 }

+ 134 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt

@@ -0,0 +1,134 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.History
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.toast
+import kotlinx.android.synthetic.main.fragment_recently_read.view.*
+
+/**
+ * Fragment that shows recently read manga.
+ * Uses R.layout.fragment_recently_read.
+ * UI related actions should be called from here.
+ */
+class RecentlyReadController : NucleusController<RecentlyReadPresenter>(),
+        FlexibleAdapter.OnUpdateListener,
+        RecentlyReadAdapter.OnRemoveClickListener,
+        RecentlyReadAdapter.OnResumeClickListener,
+        RecentlyReadAdapter.OnCoverClickListener,
+        RemoveHistoryDialog.Listener {
+
+    /**
+     * Adapter containing the recent manga.
+     */
+    var adapter: RecentlyReadAdapter? = null
+        private set
+
+    override fun getTitle(): String? {
+        return resources?.getString(R.string.label_recent_manga)
+    }
+
+    override fun createPresenter(): RecentlyReadPresenter {
+        return RecentlyReadPresenter()
+    }
+
+    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+        return inflater.inflate(R.layout.fragment_recently_read, container, false)
+    }
+
+    /**
+     * Called when view is created
+     *
+     * @param view created view
+     * @param savedViewState saved state of the view
+     */
+    override fun onViewCreated(view: View, savedViewState: Bundle?) {
+        super.onViewCreated(view, savedViewState)
+
+        with(view) {
+            // Initialize adapter
+            recycler.layoutManager = LinearLayoutManager(context)
+            adapter = RecentlyReadAdapter(this@RecentlyReadController)
+            recycler.setHasFixedSize(true)
+            recycler.adapter = adapter
+        }
+    }
+
+    override fun onDestroyView(view: View) {
+        super.onDestroyView(view)
+        adapter = null
+    }
+
+    /**
+     * Populate adapter with chapters
+     *
+     * @param mangaHistory list of manga history
+     */
+    fun onNextManga(mangaHistory: List<RecentlyReadItem>) {
+        adapter?.updateDataSet(mangaHistory.toList())
+    }
+
+    override fun onUpdateEmptyView(size: Int) {
+        val emptyView = view?.empty_view ?: return
+        if (size > 0) {
+            emptyView.hide()
+        } else {
+            emptyView.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga)
+        }
+    }
+
+    override fun onResumeClick(position: Int) {
+        val activity = activity ?: return
+        val adapter = adapter ?: return
+        if (position == RecyclerView.NO_POSITION) return
+
+        val (manga, chapter, _) = adapter.getItem(position).mch
+
+        val nextChapter = presenter.getNextChapter(chapter, manga)
+        if (nextChapter != null) {
+            val intent = ReaderActivity.newIntent(activity, manga, nextChapter)
+            startActivity(intent)
+        } else {
+            activity.toast(R.string.no_next_chapter)
+        }
+    }
+
+    override fun onRemoveClick(position: Int) {
+        val adapter = adapter ?: return
+        if (position == RecyclerView.NO_POSITION) return
+
+        val (manga, _, history) = adapter.getItem(position).mch
+
+        RemoveHistoryDialog(this, manga, history).showDialog(router)
+    }
+
+    override fun onCoverClick(position: Int) {
+        val manga = adapter?.getItem(position)?.mch?.manga ?: return
+        router.pushController(RouterTransaction.with(MangaController(manga))
+                .pushChangeHandler(FadeChangeHandler())
+                .popChangeHandler(FadeChangeHandler()))
+    }
+
+    override fun removeHistory(manga: Manga, history: History, all: Boolean) {
+        if (all) {
+            // Reset last read of chapter to 0L
+            presenter.removeAllFromHistory(manga.id!!)
+        } else {
+            // Remove all chapters belonging to manga from library
+            presenter.removeFromHistory(history)
+        }
+    }
+
+}

+ 0 - 139
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt

@@ -1,139 +0,0 @@
-package eu.kanade.tachiyomi.ui.recently_read
-
-import android.os.Bundle
-import android.support.v7.widget.LinearLayoutManager
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.History
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
-import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaActivity
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.toast
-import kotlinx.android.synthetic.main.fragment_recently_read.*
-import nucleus.factory.RequiresPresenter
-
-/**
- * Fragment that shows recently read manga.
- * Uses R.layout.fragment_recently_read.
- * UI related actions should be called from here.
- */
-@RequiresPresenter(RecentlyReadPresenter::class)
-class RecentlyReadFragment : BaseRxFragment<RecentlyReadPresenter>() {
-    companion object {
-        /**
-         * Create new RecentChaptersFragment.
-         */
-        fun newInstance(): RecentlyReadFragment {
-            return RecentlyReadFragment()
-        }
-    }
-
-    /**
-     * Adapter containing the recent manga.
-     */
-    lateinit var adapter: RecentlyReadAdapter
-        private set
-
-    /**
-     * Called when view gets created
-     *
-     * @param inflater layout inflater
-     * @param container view group
-     * @param savedState status of saved state
-     */
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
-        return inflater.inflate(R.layout.fragment_recently_read, container, false)
-    }
-
-    /**
-     * Called when view is created
-     *
-     * @param view created view
-     * @param savedState status of saved sate
-     */
-    override fun onViewCreated(view: View?, savedState: Bundle?) {
-        // Initialize adapter
-        recycler.layoutManager = LinearLayoutManager(activity)
-        adapter = RecentlyReadAdapter(this)
-        recycler.setHasFixedSize(true)
-        recycler.adapter = adapter
-
-        // Update toolbar text
-        setToolbarTitle(R.string.label_recent_manga)
-    }
-
-    /**
-     * Populate adapter with chapters
-     *
-     * @param mangaHistory list of manga history
-     */
-    fun onNextManga(mangaHistory: List<MangaChapterHistory>) {
-        (activity as MainActivity).updateEmptyView(mangaHistory.isEmpty(),
-                R.string.information_no_recent_manga, R.drawable.ic_glasses_black_128dp)
-
-        adapter.setItems(mangaHistory)
-    }
-
-    /**
-     * Reset last read of chapter to 0L
-     * @param history history belonging to chapter
-     */
-    fun removeFromHistory(history: History) {
-        presenter.removeFromHistory(history)
-    }
-
-    /**
-     * Removes all chapters belonging to manga from library
-     * @param mangaId id of manga
-     */
-    fun removeAllFromHistory(mangaId: Long) {
-        presenter.removeAllFromHistory(mangaId)
-    }
-
-    /**
-     * Open chapter to continue reading
-     * @param chapter chapter that is opened
-     * @param manga manga belonging to chapter
-     */
-    fun openChapter(chapter: Chapter, manga: Manga) {
-        if (!chapter.read) {
-            val intent = ReaderActivity.newIntent(activity, manga, chapter)
-            startActivity(intent)
-        } else {
-            presenter.openNextChapter(chapter, manga)
-        }
-    }
-
-    /**
-     * Called from the presenter when wanting to open the next chapter of the current one.
-     * @param chapter the next chapter or null if it doesn't exist.
-     * @param manga the manga of the chapter.
-     */
-    fun onOpenNextChapter(chapter: Chapter?, manga: Manga) {
-        if (chapter == null) {
-            context.toast(R.string.no_next_chapter)
-        }
-        // Avoid crashes if the fragment isn't resumed, the event will be ignored but it's unlikely
-        // to happen.
-        else if (isResumed) {
-            val intent = ReaderActivity.newIntent(activity, manga, chapter)
-            startActivity(intent)
-        }
-    }
-
-    /**
-     * Open manga info page
-     * @param manga manga belonging to info page
-     */
-    fun openMangaInfo(manga: Manga) {
-        val intent = MangaActivity.newIntent(activity, manga, true)
-        startActivity(intent)
-    }
-
-}

+ 22 - 53
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt

@@ -1,17 +1,12 @@
 package eu.kanade.tachiyomi.ui.recently_read
 
-import android.support.v7.widget.RecyclerView
 import android.view.View
-import com.afollestad.materialdialogs.MaterialDialog
 import com.bumptech.glide.Glide
 import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
-import eu.kanade.tachiyomi.widget.DialogCheckboxView
 import kotlinx.android.synthetic.main.item_recently_read.view.*
-import java.text.DateFormat
-import java.text.DecimalFormat
-import java.text.DecimalFormatSymbols
 import java.util.*
 
 /**
@@ -23,39 +18,47 @@ import java.util.*
  * @param adapter the adapter handling this holder.
  * @constructor creates a new recent chapter holder.
  */
-class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
-    : RecyclerView.ViewHolder(view) {
+class RecentlyReadHolder(
+        view: View,
+        val adapter: RecentlyReadAdapter
+) : FlexibleViewHolder(view, adapter) {
 
-    /**
-     * DecimalFormat used to display correct chapter number
-     */
-    private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
+    init {
+        itemView.remove.setOnClickListener {
+            adapter.removeClickListener.onRemoveClick(adapterPosition)
+        }
 
-    private val df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
+        itemView.resume.setOnClickListener {
+            adapter.resumeClickListener.onResumeClick(adapterPosition)
+        }
+
+        itemView.cover.setOnClickListener {
+            adapter.coverClickListener.onCoverClick(adapterPosition)
+        }
+    }
 
     /**
      * Set values of view
      *
      * @param item item containing history information
      */
-    fun onSetValues(item: MangaChapterHistory) {
+    fun bind(item: MangaChapterHistory) {
         // Retrieve objects
-        val manga = item.manga
-        val chapter = item.chapter
-        val history = item.history
+        val (manga, chapter, history) = item
 
         // Set manga title
         itemView.manga_title.text = manga.title
 
         // Set source + chapter title
-        val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
+        val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
         itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source)
                 .format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber)
 
         // Set last read timestamp title
-        itemView.last_read.text = df.format(Date(history.last_read))
+        itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read))
 
         // Set cover
+        Glide.clear(itemView.cover)
         if (!manga.thumbnail_url.isNullOrEmpty()) {
             Glide.with(itemView.context)
                     .load(manga)
@@ -64,40 +67,6 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
                     .into(itemView.cover)
         }
 
-        // Set remove clickListener
-        itemView.remove.setOnClickListener {
-            // Create custom view
-            val dialogCheckboxView = DialogCheckboxView(itemView.context).apply {
-                setDescription(R.string.dialog_with_checkbox_remove_description)
-                setOptionDescription(R.string.dialog_with_checkbox_reset)
-            }
-            MaterialDialog.Builder(itemView.context)
-                    .title(R.string.action_remove)
-                    .customView(dialogCheckboxView, true)
-                    .positiveText(R.string.action_remove)
-                    .negativeText(android.R.string.cancel)
-                    .onPositive { materialDialog, dialogAction ->
-                        // Check if user wants all chapters reset
-                        if (dialogCheckboxView.isChecked()) {
-                            adapter.fragment.removeAllFromHistory(manga.id!!)
-                        } else {
-                            adapter.fragment.removeFromHistory(history)
-                        }
-                    }
-                    .onNegative { materialDialog, dialogAction ->
-                        materialDialog.dismiss()
-                    }.show()
-        }
-
-        // Set continue reading clickListener
-        itemView.resume.setOnClickListener {
-            adapter.fragment.openChapter(chapter, manga)
-        }
-
-        // Set open manga info clickListener
-        itemView.cover.setOnClickListener {
-            adapter.fragment.openMangaInfo(manga)
-        }
     }
 
 }

+ 43 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt

@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.util.inflate
+
+class RecentlyReadItem(val mch: MangaChapterHistory) : AbstractFlexibleItem<RecentlyReadHolder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.item_recently_read
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>,
+                                  inflater: LayoutInflater,
+                                  parent: ViewGroup): RecentlyReadHolder {
+
+        val view = parent.inflate(layoutRes)
+        return RecentlyReadHolder(view, adapter as RecentlyReadAdapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+                                holder: RecentlyReadHolder,
+                                position: Int,
+                                payloads: List<Any?>?) {
+
+        holder.bind(mch)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other is RecentlyReadItem) {
+            return mch.manga.id == other.mch.manga.id
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return mch.manga.id!!.hashCode()
+    }
+}

+ 25 - 39
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt

@@ -5,11 +5,9 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.History
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import rx.Observable
 import rx.android.schedulers.AndroidSchedulers
-import timber.log.Timber
 import uy.kohesive.injekt.injectLazy
 import java.util.*
 
@@ -18,7 +16,7 @@ import java.util.*
  * Contains information and data for fragment.
  * Observable updates should be called from here.
  */
-class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
+class RecentlyReadPresenter : BasePresenter<RecentlyReadController>() {
 
     /**
      * Used to connect to database
@@ -30,22 +28,21 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
 
         // Used to get a list of recently read manga
         getRecentMangaObservable()
-                .subscribeLatestCache({ view, historyList ->
-                    view.onNextManga(historyList)
-                })
+                .subscribeLatestCache(RecentlyReadController::onNextManga)
     }
 
     /**
      * Get recent manga observable
      * @return list of history
      */
-    fun getRecentMangaObservable(): Observable<List<MangaChapterHistory>> {
+    fun getRecentMangaObservable(): Observable<List<RecentlyReadItem>> {
         // Set date for recent manga
         val cal = Calendar.getInstance()
         cal.time = Date()
         cal.add(Calendar.MONTH, -1)
 
         return db.getRecentManga(cal.time).asRxObservable()
+                .map { it.map(::RecentlyReadItem) }
                 .observeOn(AndroidSchedulers.mainThread())
     }
 
@@ -73,50 +70,39 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
     }
 
     /**
-     * Open the next chapter instead of the current one.
+     * Retrieves the next chapter of the given one.
+     *
      * @param chapter the chapter of the history object.
      * @param manga the manga of the chapter.
      */
-    fun openNextChapter(chapter: Chapter, manga: Manga) {
+    fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
+        if (!chapter.read) {
+            return chapter
+        }
+
         val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
             Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
             Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
             else -> throw NotImplementedError("Unknown sorting method")
         }
 
-        db.getChapters(manga).asRxSingle()
-                .map { it.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) }) }
-                .map { chapters ->
-                    val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
-                    when (manga.sorting) {
-                        Manga.SORTING_SOURCE -> {
-                            chapters.getOrNull(currChapterIndex + 1)
-                        }
-                        Manga.SORTING_NUMBER -> {
-                            val chapterNumber = chapter.chapter_number
+        val chapters = db.getChapters(manga).executeAsBlocking()
+                .sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
 
-                            var nextChapter: Chapter? = null
-                            for (i in (currChapterIndex + 1) until chapters.size) {
-                                val c = chapters[i]
-                                if (c.chapter_number > chapterNumber &&
-                                        c.chapter_number <= chapterNumber + 1) {
+        val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
+        return when (manga.sorting) {
+            Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
+            Manga.SORTING_NUMBER -> {
+                val chapterNumber = chapter.chapter_number
 
-                                    nextChapter = c
-                                    break
-                                }
-                            }
-                            nextChapter
+                ((currChapterIndex + 1) until chapters.size)
+                        .map { chapters[it] }
+                        .firstOrNull { it.chapter_number > chapterNumber &&
+                                it.chapter_number <= chapterNumber + 1
                         }
-                        else -> throw NotImplementedError("Unknown sorting method")
-                    }
-                }
-                .toObservable()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribeFirst({ view, chapter ->
-                    view.onOpenNextChapter(chapter, manga)
-                }, { view, error ->
-                    Timber.e(error)
-                })
+            }
+            else -> throw NotImplementedError("Unknown sorting method")
+        }
     }
 
 }

+ 56 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt

@@ -0,0 +1,56 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.History
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import eu.kanade.tachiyomi.widget.DialogCheckboxView
+
+class RemoveHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
+        where T : Controller, T: RemoveHistoryDialog.Listener {
+
+    private var manga: Manga? = null
+
+    private var history: History? = null
+
+    constructor(target: T, manga: Manga, history: History) : this() {
+        this.manga = manga
+        this.history = history
+        targetController = target
+    }
+
+    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+        val activity = activity!!
+
+        // Create custom view
+        val dialogCheckboxView = DialogCheckboxView(activity).apply {
+            setDescription(R.string.dialog_with_checkbox_remove_description)
+            setOptionDescription(R.string.dialog_with_checkbox_reset)
+        }
+
+        return MaterialDialog.Builder(activity)
+                .title(R.string.action_remove)
+                .customView(dialogCheckboxView, true)
+                .positiveText(R.string.action_remove)
+                .negativeText(android.R.string.cancel)
+                .onPositive { _, _ -> onPositive(dialogCheckboxView.isChecked()) }
+                .build()
+    }
+
+    private fun onPositive(checked: Boolean) {
+        val target = targetController as? Listener ?: return
+        val manga = manga ?: return
+        val history = history ?: return
+
+        target.removeHistory(manga, history, checked)
+    }
+
+    interface Listener {
+        fun removeHistory(manga: Manga, history: History, all: Boolean)
+    }
+
+}

+ 0 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt

@@ -7,7 +7,6 @@ import android.support.v7.preference.XpPreferenceFragment
 import android.view.View
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
-import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.util.LocaleHelper

+ 23 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt

@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.widget
+
+import android.support.v4.widget.DrawerLayout
+import android.view.View
+import android.view.ViewGroup
+
+class DrawerSwipeCloseListener(
+        private val drawer: DrawerLayout,
+        private val navigationView: ViewGroup
+) : DrawerLayout.SimpleDrawerListener() {
+
+    override fun onDrawerOpened(drawerView: View) {
+        if (drawerView == navigationView) {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, drawerView)
+        }
+    }
+
+    override fun onDrawerClosed(drawerView: View) {
+        if (drawerView == navigationView) {
+            drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, drawerView)
+        }
+    }
+}

+ 4 - 10
app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt

@@ -1,11 +1,11 @@
 package eu.kanade.tachiyomi.widget
 
-import android.support.v4.view.PagerAdapter
 import android.view.View
 import android.view.ViewGroup
+import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter
 import java.util.*
 
-abstract class RecyclerViewPagerAdapter : PagerAdapter() {
+abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
 
     private val pool = Stack<View>()
 
@@ -21,22 +21,16 @@ abstract class RecyclerViewPagerAdapter : PagerAdapter() {
 
     protected open fun recycleView(view: View, position: Int) {}
 
-    override fun instantiateItem(container: ViewGroup, position: Int): Any {
+    override fun createView(container: ViewGroup, position: Int): View {
         val view = if (pool.isNotEmpty()) pool.pop() else createView(container)
         bindView(view, position)
-        container.addView(view)
         return view
     }
 
-    override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
-        val view = obj as View
+    override fun destroyView(container: ViewGroup, position: Int, view: View) {
         recycleView(view, position)
-        container.removeView(view)
         if (recycle) pool.push(view)
     }
 
-    override fun isViewFromObject(view: View, obj: Any): Boolean {
-        return view === obj
-    }
 
 }

+ 281 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java

@@ -0,0 +1,281 @@
+/*
+ * Copyright 2016 Davide Steduto
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package eu.kanade.tachiyomi.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntDef;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+import eu.davidea.flexibleadapter.FlexibleAdapter;
+
+/**
+ * Helper to simplify the Undo operation with FlexibleAdapter.
+ *
+ * @author Davide Steduto
+ * @since 30/04/2016
+ */
+@SuppressWarnings("WeakerAccess")
+public class UndoHelper extends Snackbar.Callback {
+
+    /**
+     * Default undo-timeout of 5''.
+     */
+    public static final int UNDO_TIMEOUT = 5000;
+    /**
+     * Indicates that the Confirmation Listener (Undo and Delete) will perform a deletion.
+     */
+    public static final int ACTION_REMOVE = 0;
+    /**
+     * Indicates that the Confirmation Listener (Undo and Delete) will perform an update.
+     */
+    public static final int ACTION_UPDATE = 1;
+
+    /**
+     * Annotation interface for Undo actions.
+     */
+    @IntDef({ACTION_REMOVE, ACTION_UPDATE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {
+    }
+
+    @Action
+    private int mAction = ACTION_REMOVE;
+    private List<Integer> mPositions = null;
+    private Object mPayload = null;
+    private FlexibleAdapter mAdapter;
+    private Snackbar mSnackbar = null;
+    private OnActionListener mActionListener;
+    private OnUndoListener mUndoListener;
+    private @ColorInt int mActionTextColor = Color.TRANSPARENT;
+
+
+    /**
+     * Default constructor.
+     * <p>By calling this constructor, {@link FlexibleAdapter#setPermanentDelete(boolean)}
+     * is set {@code false} automatically.
+     *
+     * @param adapter      the instance of {@code FlexibleAdapter}
+     * @param undoListener the callback for the Undo and Delete confirmation
+     */
+    public UndoHelper(FlexibleAdapter adapter, OnUndoListener undoListener) {
+        this.mAdapter = adapter;
+        this.mUndoListener = undoListener;
+        adapter.setPermanentDelete(false);
+    }
+
+    /**
+     * Sets the payload to inform other linked items about the change in action.
+     *
+     * @param payload any non-null user object to notify the parent (the payload will be
+     *                therefore passed to the bind method of the parent ViewHolder),
+     *                pass null to <u>not</u> notify the parent
+     * @return this object, so it can be chained
+     */
+    public UndoHelper withPayload(Object payload) {
+        this.mPayload = payload;
+        return this;
+    }
+
+    /**
+     * By default {@link UndoHelper#ACTION_REMOVE} is performed.
+     *
+     * @param action         the action, one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE}
+     * @param actionListener the listener for the custom action to perform before the deletion
+     * @return this object, so it can be chained
+     */
+    public UndoHelper withAction(@Action int action, @NonNull OnActionListener actionListener) {
+        this.mAction = action;
+        this.mActionListener = actionListener;
+        return this;
+    }
+
+    /**
+     * Sets the text color of the action.
+     *
+     * @param color the color for the action button
+     * @return this object, so it can be chained
+     */
+    public UndoHelper withActionTextColor(@ColorInt int color) {
+        this.mActionTextColor = color;
+        return this;
+    }
+
+    /**
+     * As {@link #remove(List, View, CharSequence, CharSequence, int)} but with String
+     * resources instead of CharSequence.
+     */
+    public void remove(List<Integer> positions, @NonNull View mainView,
+                       @StringRes int messageStringResId, @StringRes int actionStringResId,
+                       @IntRange(from = -1) int undoTime) {
+        Context context = mainView.getContext();
+        remove(positions, mainView, context.getString(messageStringResId),
+                context.getString(actionStringResId), undoTime);
+    }
+
+    /**
+     * Performs the action on the specified positions and displays a SnackBar to Undo
+     * the operation. To customize the UPDATE event, please set a custom listener with
+     * {@link #withAction(int, OnActionListener)} method.
+     * <p>By default the DELETE action will be performed.</p>
+     *
+     * @param positions  the position to delete or update
+     * @param mainView   the view to find a parent from
+     * @param message    the text to show. Can be formatted text
+     * @param actionText the action text to display
+     * @param undoTime   How long to display the message. Either {@link Snackbar#LENGTH_SHORT} or
+     *                   {@link Snackbar#LENGTH_LONG} or any custom Integer.
+     * @see #remove(List, View, int, int, int)
+     */
+    @SuppressWarnings("WrongConstant")
+    public void remove(List<Integer> positions, @NonNull View mainView,
+                       CharSequence message, CharSequence actionText,
+                       @IntRange(from = -1) int undoTime) {
+        this.mPositions = positions;
+        Snackbar snackbar;
+        if (!mAdapter.isPermanentDelete()) {
+            snackbar = Snackbar.make(mainView, message, undoTime > 0 ? undoTime + 400 : undoTime)
+                    .setAction(actionText, new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            if (mUndoListener != null)
+                                mUndoListener.onUndoConfirmed(mAction);
+                        }
+                    });
+        } else {
+            snackbar = Snackbar.make(mainView, message, undoTime);
+        }
+        if (mActionTextColor != Color.TRANSPARENT) {
+            snackbar.setActionTextColor(mActionTextColor);
+        }
+        mSnackbar = snackbar;
+        snackbar.addCallback(this);
+        snackbar.show();
+    }
+
+    public void dismissNow() {
+        if (mSnackbar != null) {
+            mSnackbar.removeCallback(this);
+            mSnackbar.dismiss();
+            onDismissed(mSnackbar, Snackbar.Callback.DISMISS_EVENT_MANUAL);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onDismissed(Snackbar snackbar, int event) {
+        if (mAdapter.isPermanentDelete()) return;
+        switch (event) {
+            case DISMISS_EVENT_SWIPE:
+            case DISMISS_EVENT_MANUAL:
+            case DISMISS_EVENT_TIMEOUT:
+                if (mUndoListener != null)
+                    mUndoListener.onDeleteConfirmed(mAction);
+                mAdapter.emptyBin();
+                mSnackbar = null;
+            case DISMISS_EVENT_CONSECUTIVE:
+            case DISMISS_EVENT_ACTION:
+            default:
+                break;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onShown(Snackbar snackbar) {
+        boolean consumed = false;
+        // Perform the action before deletion
+        if (mActionListener != null) consumed = mActionListener.onPreAction();
+        // Remove selected items from Adapter list after SnackBar is shown
+        if (!consumed) mAdapter.removeItems(mPositions, mPayload);
+        // Perform the action after the deletion
+        if (mActionListener != null) mActionListener.onPostAction();
+        // Here, we can notify the callback only in case of permanent deletion
+        if (mAdapter.isPermanentDelete() && mUndoListener != null)
+            mUndoListener.onDeleteConfirmed(mAction);
+    }
+
+    /**
+     * Basic implementation of {@link OnActionListener} interface.
+     * <p>Override the methods as your convenience.</p>
+     */
+    public static class SimpleActionListener implements OnActionListener {
+        @Override
+        public boolean onPreAction() {
+            return false;
+        }
+
+        @Override
+        public void onPostAction() {
+
+        }
+    }
+
+    public interface OnActionListener {
+        /**
+         * Performs the custom action before item deletion.
+         *
+         * @return true if action has been consumed and should stop the deletion, false to
+         * continue with the deletion
+         */
+        boolean onPreAction();
+
+        /**
+         * Performs custom action After items deletion. Useful to finish the action mode and perform
+         * secondary custom actions.
+         */
+        void onPostAction();
+    }
+
+    /**
+     * @since 30/04/2016
+     */
+    public interface OnUndoListener {
+        /**
+         * Called when Undo event is triggered. Perform custom action after restoration.
+         * <p>Usually for a delete restoration you should call
+         * {@link FlexibleAdapter#restoreDeletedItems()}.</p>
+         *
+         * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE}
+         */
+        void onUndoConfirmed(int action);
+
+        /**
+         * Called when Undo timeout is over and action must be committed in the user Database.
+         * <p>Due to Java Generic, it's too complicated and not well manageable if we pass the
+         * List&lt;T&gt; object.<br/>
+         * So, to get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the
+         * implementation of this method.</p>
+         *
+         * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE}
+         */
+        void onDeleteConfirmed(int action);
+    }
+
+}

+ 4 - 14
app/src/main/res/layout/activity_edit_categories.xml

@@ -2,27 +2,17 @@
 <android.support.design.widget.CoordinatorLayout
     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="match_parent"
-    android:gravity="center"
     android:fitsSystemWindows="true">
 
     <include layout="@layout/toolbar"/>
 
-    <android.support.v7.widget.RecyclerView
+    <com.bluelinelabs.conductor.ChangeHandlerFrameLayout
+        android:id="@+id/controller_container"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="?attr/actionBarSize"
-        android:id="@+id/recycler"
-        android:choiceMode="multipleChoice"
-        tools:listitem="@layout/item_edit_categories"
-    />
-
-    <android.support.design.widget.FloatingActionButton
-        android:id="@+id/fab"
-        app:layout_anchor="@id/recycler"
-        app:srcCompat="@drawable/ic_add_white_24dp"
-        style="@style/Theme.Widget.FAB"/>
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        />
 
 </android.support.design.widget.CoordinatorLayout>

Some files were not shown because too many files changed in this diff