> : NucleusAppCompatActivityImplementations 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);
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NoToolbarElevationController.kt
new file mode 100644
index 0000000000..c036123891
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
new file mode 100644
index 0000000000..63eba25ed2
--- /dev/null
+++ b/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
>(val bundle: Bundle? = null) : RxController(),
+ PresenterFactory
{
+
+ private val delegate = NucleusConductorDelegate(this)
+
+ val presenter: P
+ get() = delegate.presenter
+
+ init {
+ addLifecycleListener(NucleusConductorLifecycleListener(delegate))
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RouterPagerAdapter.java
new file mode 100644
index 0000000000..cf265fc3de
--- /dev/null
+++ b/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 savedPages = new SparseArray<>();
+ private SparseArray visibleRouters = new SparseArray<>();
+ private ArrayList 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 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 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;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/RxController.kt
new file mode 100644
index 0000000000..80d3b31d41
--- /dev/null
+++ b/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 Observable.subscribeUntilDetach(): Subscription {
+
+ return subscribe().also { untilDetachSubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
+
+ return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDetach(onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit): Subscription {
+
+ return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDetach(onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit,
+ onCompleted: () -> Unit): Subscription {
+
+ return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDestroy(): Subscription {
+
+ return subscribe().also { untilDestroySubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
+
+ return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit): Subscription {
+
+ return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
+ }
+
+ fun Observable.subscribeUntilDestroy(onNext: (T) -> Unit,
+ onError: (Throwable) -> Unit,
+ onCompleted: () -> Unit): Subscription {
+
+ return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SecondaryDrawerController.kt
new file mode 100644
index 0000000000..ba2ce016a1
--- /dev/null
+++ b/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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/TabbedController.kt
new file mode 100644
index 0000000000..02fba36c31
--- /dev/null
+++ b/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) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt
deleted file mode 100644
index ee466c5360..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package eu.kanade.tachiyomi.ui.base.fragment
-
-import android.support.v4.app.Fragment
-
-abstract class BaseFragment : Fragment(), FragmentMixin {
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt
deleted file mode 100644
index 672c44731e..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.kt
+++ /dev/null
@@ -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> : NucleusSupportFragment
(), 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)
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt
deleted file mode 100644
index 24c766182a..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/FragmentMixin.kt
+++ /dev/null
@@ -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
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt
index fbf756a5be..1d365b43d9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt
+++ b/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> : RxPresenter() {
-
- lateinit var context: Context
+open class BasePresenter : RxPresenter() {
/**
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.java
new file mode 100644
index 0000000000..62a50af839
--- /dev/null
+++ b/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 {
+
+ @Nullable private P presenter;
+ @Nullable private Bundle bundle;
+ private boolean presenterHasView = false;
+
+ private PresenterFactory
factory;
+
+ public NucleusConductorDelegate(PresenterFactory
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;
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.java
new file mode 100644
index 0000000000..33272a1b20
--- /dev/null
+++ b/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));
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
similarity index 56%
rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
index a14dc0c426..1d5e50e237 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
+++ b/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(),
- FlexibleAdapter.OnItemClickListener,
- FlexibleAdapter.OnItemLongClickListener,
- FlexibleAdapter.EndlessScrollListener {
-
- /**
- * 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>
-
- /**
- * 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()
-
- /**
- * 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 = this@CatalogueFragment.adapter }
-
- (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) {
- 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 {
- 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, 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(bundle),
+ SecondaryDrawerController,
+ FlexibleAdapter.OnItemClickListener,
+ FlexibleAdapter.OnItemLongClickListener,
+ FlexibleAdapter.EndlessScrollListener,
+ ChangeMangaCategoriesDialog.Listener {
+
+ /**
+ * Preferences helper.
+ */
+ private val preferences: PreferencesHelper by injectLazy()
+
+ /**
+ * Adapter containing the list of manga from the catalogue.
+ */
+ private var adapter: FlexibleAdapter>? = 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 = this@CatalogueController.adapter }
+
+ (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) {
+ 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 {
+ 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, categories: List) {
+ val manga = mangas.firstOrNull() ?: return
+ presenter.updateMangaCategories(manga, categories)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
index 5aa1eecd1c..eb00270596 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt
+++ b/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()
return R.layout.item_catalogue_grid
}
- override fun createViewHolder(adapter: FlexibleAdapter>, 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()
}
}
- override fun bindViewHolder(adapter: FlexibleAdapter>, holder: CatalogueHolder, position: Int, payloads: List?) {
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>,
+ holder: CatalogueHolder,
+ position: Int,
+ payloads: List?) {
+
holder.onSetValues(manga)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
index daa6d58862..b9a08bdef3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
+++ b/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() {
-
- /**
- * 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() {
/**
* Enabled sources.
@@ -182,7 +168,7 @@ open class CataloguePresenter : BasePresenter() {
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() {
* @return List of categories, default plus user categories
*/
fun getCategories(): List {
- return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
+ return db.getCategories().executeAsBlocking()
}
/**
@@ -415,10 +401,7 @@ open class CataloguePresenter : BasePresenter() {
*/
fun getMangaCategoryIds(manga: Manga): Array {
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() {
* @param categories the selected categories.
* @param manga the manga to move.
*/
- fun moveMangaToCategories(categories: List, manga: Manga) {
- val mc = categories.map { MangaCategory.create(manga, it) }
-
- db.setMangaCategories(mc, arrayListOf(manga))
+ fun moveMangaToCategories(manga: Manga, categories: List) {
+ 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() {
* @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() {
if (!manga.favorite)
changeMangaFavorite(manga)
- moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
+ moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
changeMangaFavorite(manga)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt
deleted file mode 100644
index a8cceabe53..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt
+++ /dev/null
@@ -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(),
- 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) {
- 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 })
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
index 5f3b89feed..907a5a04a0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
+++ b/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(null, activity, true) {
+class CategoryAdapter(controller: CategoryController) :
+ FlexibleAdapter(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)
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt
new file mode 100644
index 0000000000..81243c50d4
--- /dev/null
+++ b/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(),
+ 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) {
+ 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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryCreateDialog.kt
new file mode 100644
index 0000000000..dfa4bad32a
--- /dev/null
+++ b/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(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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
index 35f58a7b5a..3ce4062736 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
+++ b/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)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
index 3dd3ad5a74..a21be31ceb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryItem.kt
+++ b/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() {
+ /**
+ * 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?) {
+ /**
+ * 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?) {
+
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
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt
index b8691aa5d0..64c7af09a6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt
+++ b/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() {
-
- /**
- * Used to connect to database.
- */
- private val db: DatabaseHelper by injectLazy()
+class CategoryPresenter(
+ private val db: DatabaseHelper = Injekt.get()
+) : BasePresenter() {
/**
* List containing categories.
*/
private var categories: List = 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() {
.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() {
}
/**
- * 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) {
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) {
categories.forEachIndexed { i, category ->
@@ -81,19 +81,27 @@ class CategoryPresenter : BasePresenter() {
}
/**
- * 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) }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryRenameDialog.kt
new file mode 100644
index 0000000000..286093b06b
--- /dev/null
+++ b/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(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"
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt
index 9a8d010fef..30cfe11a95 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt
+++ b/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() {
}
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()
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
new file mode 100644
index 0000000000..730b5e9910
--- /dev/null
+++ b/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) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt
deleted file mode 100644
index b567e79d1c..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesFragment.kt
+++ /dev/null
@@ -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()
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
index bcb27c418f..924425b62b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
+++ b/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() {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt
new file mode 100644
index 0000000000..08f933c8ec
--- /dev/null
+++ b/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(bundle: Bundle? = null) :
+ DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
+
+ private var mangas = emptyList()
+
+ private var categories = emptyList()
+
+ private var preselected = emptyArray()
+
+ constructor(target: T, mangas: List, categories: List,
+ preselected: Array) : 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, categories: List)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/DeleteLibraryMangasDialog.kt
new file mode 100644
index 0000000000..1aa376eb87
--- /dev/null
+++ b/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(bundle: Bundle? = null) :
+ DialogController(bundle) where T : Controller, T: DeleteLibraryMangasDialog.Listener {
+
+ private var mangas = emptyList()
+
+ constructor(target: T, mangas: List) : 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, deleteChapters: Boolean)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt
index fe4433eadf..67571b8bd5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt
+++ b/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 = 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 = 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
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
index 8aab567e82..61731d87d2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
+++ b/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() {
-
- /**
- * The list of manga in this category.
- */
- private var mangas: List = emptyList()
-
- init {
- setHasStableIds(true)
- }
-
- /**
- * Sets a list of manga in the adapter.
- *
- * @param list the list to set.
- */
- fun setItems(list: List) {
- 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(null, view, true) {
+
+ /**
+ * The list of manga in this category.
+ */
+ private var mangas: List = emptyList()
+
+ /**
+ * Sets a list of manga in the adapter.
+ *
+ * @param list the list to set.
+ */
+ fun setItems(list: List) {
+ // 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) })
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
index bdafd0bc0d..feb4025d97 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
+++ b/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()
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
new file mode 100644
index 0000000000..4ce6cedd86
--- /dev/null
+++ b/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(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()
+
+ private var selectedCoverManga: Manga? = null
+
+ /**
+ * Relay to notify the UI of selection updates.
+ */
+ val selectionRelay: PublishRelay = PublishRelay.create()
+
+ /**
+ * Relay to notify search query changes.
+ */
+ val searchRelay: BehaviorRelay = BehaviorRelay.create()
+
+ /**
+ * Relay to notify the library's viewpager for updates.
+ */
+ val libraryMangaRelay: BehaviorRelay = 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, mangaMap: Map>) {
+ 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 {
+ 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, categories: List) {
+ presenter.moveMangasToCategories(categories, mangas)
+ destroyActionModeIfNeeded()
+ }
+
+ override fun deleteMangasFromLibrary(mangas: List, 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
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt
deleted file mode 100644
index 0b6d92fe46..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt
+++ /dev/null
@@ -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(), 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 {
- 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, mangaMap: Map>) {
- // 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) {
- 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) {
- // 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()
- }
-
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
index 91424f3bd6..d74dc478a3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
+++ b/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)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
index efdd42200b..2359377da0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
+++ b/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)
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
new file mode 100644
index 0000000000..0669d068bd
--- /dev/null
+++ b/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(), 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?) {
+
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
index 22dd444c90..885fdb2f1c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
+++ b/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)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt
index 2b1be71e10..e5dffe3086 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryMangaEvent.kt
+++ b/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>) {
+class LibraryMangaEvent(val mangas: Map>) {
- fun getMangaForCategory(category: Category): List? {
+ fun getMangaForCategory(category: Category): List? {
return mangas[category.id]
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
index 8be382b6f6..a7c32b2e13 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
+++ b/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() {
-
- /**
- * 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 = emptyList()
-
- /**
- * Currently selected manga.
- */
- val selectedMangas = mutableListOf()
-
- /**
- * Search query of the library.
- */
- val searchSubject: BehaviorRelay = BehaviorRelay.create()
-
- /**
- * Subject to notify the library's viewpager for updates.
- */
- val libraryMangaSubject: BehaviorRelay = BehaviorRelay.create()
-
- /**
- * Subject to notify the UI of selection updates.
- */
- val selectionSubject: PublishRelay = 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>): Map> {
- // Cached list of downloaded manga directories given a source id.
- val mangaDirsForSource = mutableMapOf>()
-
- // Cached list of downloaded chapter directories for a manga.
- val chapterDirectories = mutableMapOf()
-
- 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>): Map> {
- 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, Map>>> {
- 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> {
- 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