diff --git a/app/build.gradle b/app/build.gradle index f8db49c325..6ee2111ba7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,6 +100,16 @@ android { dependencies { + compile "com.bluelinelabs:conductor:2.1.3" + + final rxbindings_version = '1.0.1' + compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" + compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" + compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" + compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" + + compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' + // Modified dependencies compile 'com.github.inorichi:subsampling-scale-image-view:01e5385' compile 'com.github.inorichi:junrar-android:634c1f5' @@ -212,7 +222,7 @@ dependencies { } buildscript { - ext.kotlin_version = '1.1.1' + ext.kotlin_version = '1.1.2' repositories { mavenCentral() } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21627600d8..23b3854e55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,10 +32,6 @@ - @@ -43,10 +39,6 @@ android:name=".ui.setting.SettingsActivity" android:label="@string/label_settings" android:parentActivityName=".ui.main.MainActivity" /> - > : NucleusAppCompatActivity

Implementations should override this class and implement {@link #onCreateDialog(Bundle)} to create a custom dialog, such as an {@link android.app.AlertDialog} + */ +public abstract class DialogController extends RestoreViewOnCreateController { + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + + private Dialog dialog; + private boolean dismissed; + + /** + * Convenience constructor for use when no arguments are needed. + */ + protected DialogController() { + super(null); + } + + /** + * Constructor that takes arguments that need to be retained across restarts. + * + * @param args Any arguments that need to be retained. + */ + protected DialogController(@Nullable Bundle args) { + super(args); + } + + @NonNull + @Override + final protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) { + dialog = onCreateDialog(savedViewState); + //noinspection ConstantConditions + dialog.setOwnerActivity(getActivity()); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + dismissDialog(); + } + }); + if (savedViewState != null) { + Bundle dialogState = savedViewState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + dialog.onRestoreInstanceState(dialogState); + } + } + return new View(getActivity());//stub view + } + + @Override + protected void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { + super.onSaveViewState(view, outState); + Bundle dialogState = dialog.onSaveInstanceState(); + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + + @Override + protected void onAttach(@NonNull View view) { + super.onAttach(view); + dialog.show(); + } + + @Override + protected void onDetach(@NonNull View view) { + super.onDetach(view); + dialog.hide(); + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + dialog.setOnDismissListener(null); + dialog.dismiss(); + dialog = null; + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + */ + public void showDialog(@NonNull Router router) { + showDialog(router, null); + } + + /** + * Display the dialog, create a transaction and pushing the controller. + * @param router The router on which the transaction will be applied + * @param tag The tag for this controller + */ + public void showDialog(@NonNull Router router, @Nullable String tag) { + dismissed = false; + router.pushController(RouterTransaction.with(this) + .pushChangeHandler(new SimpleSwapChangeHandler(false)) + .popChangeHandler(new SimpleSwapChangeHandler(false)) + .tag(tag)); + } + + /** + * Dismiss the dialog and pop this controller + */ + public void dismissDialog() { + if (dismissed) { + return; + } + getRouter().popController(this); + dismissed = true; + } + + @Nullable + protected Dialog getDialog() { + return dialog; + } + + /** + * Build your own custom Dialog container such as an {@link android.app.AlertDialog} + * + * @param savedViewState A bundle for the view's state, which would have been created in {@link #onSaveViewState(View, Bundle)} or {@code null} if no saved state exists. + * @return Return a new Dialog instance to be displayed by the Controller + */ + @NonNull + protected abstract Dialog onCreateDialog(@Nullable Bundle savedViewState); +} 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>> { - return db.getLibraryMangas().asRxObservable() - .map { list -> list.groupBy { it.category } } - } - - /** - * Requests the library to be filtered. - */ - fun requestFilterUpdate() { - filterTriggerRelay.call(Unit) - } - - /** - * Requests the library to be sorted. - */ - fun requestSortUpdate() { - sortTriggerRelay.call(Unit) - } - - /** - * Called when a manga is opened. - */ - fun onOpenManga() { - // Avoid further db updates for the library when it's not needed - librarySubscription?.let { remove(it) } - } - - /** - * Sets the selection for a given manga. - * - * @param manga the manga whose selection has changed. - * @param selected whether it's now selected or not. - */ - fun setSelection(manga: Manga, selected: Boolean) { - if (selected) { - selectedMangas.add(manga) - selectionSubject.call(LibrarySelectionEvent.Selected(manga)) - } else { - selectedMangas.remove(manga) - selectionSubject.call(LibrarySelectionEvent.Unselected(manga)) - } - } - - /** - * Clears all the manga selections and notifies the UI. - */ - fun clearSelections() { - selectedMangas.clear() - selectionSubject.call(LibrarySelectionEvent.Cleared()) - } - - /** - * Returns the common categories for the given list of manga. - * - * @param mangas the list of manga. - */ - fun getCommonCategories(mangas: List): Collection { - if (mangas.isEmpty()) return emptyList() - return mangas.toSet() - .map { db.getCategoriesForManga(it).executeAsBlocking() } - .reduce { set1: Iterable, set2 -> set1.intersect(set2) } - } - - /** - * Remove the selected manga from the library. - * - * @param deleteChapters whether to also delete downloaded chapters. - */ - fun removeMangaFromLibrary(deleteChapters: Boolean) { - // Create a set of the list - val mangaToDelete = selectedMangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } - - Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } - .onErrorResumeNext { Observable.empty() } - .subscribeOn(Schedulers.io()) - .subscribe() - - Observable.fromCallable { - mangaToDelete.forEach { manga -> - coverCache.deleteFromCache(manga.thumbnail_url) - if (deleteChapters) { - val source = sourceManager.get(manga.source) as? HttpSource - if (source != null) { - downloadManager.findMangaDir(source, manga)?.delete() - } - } - } - } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Move the given list of manga to categories. - * - * @param categories the selected categories. - * @param mangas the list of manga to move. - */ - fun moveMangasToCategories(categories: List, mangas: List) { - val mc = ArrayList() - - for (manga in mangas) { - for (cat in categories) { - mc.add(MangaCategory.create(manga, cat)) - } - } - - db.setMangaCategories(mc, mangas) - } - - /** - * Update cover with local file. - * - * @param inputStream the new cover. - * @param manga the manga edited. - * @return true if the cover is updated, false otherwise - */ - @Throws(IOException::class) - fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(context, manga, inputStream) - return true - } - - if (manga.thumbnail_url != null && manga.favorite) { - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - return true - } - return false - } - -} +package eu.kanade.tachiyomi.ui.library + +import android.os.Bundle +import android.util.Pair +import com.hippo.unifile.UniFile +import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.combineLatest +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.IOException +import java.io.InputStream +import java.util.* + +/** + * Presenter of [LibraryController]. + */ +class LibraryPresenter( + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + /** + * Categories of the library. + */ + var categories: List = emptyList() + private set + + /** + * Relay used to apply the UI filters to the last emission of the library. + */ + private val filterTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Relay used to apply the selected sorting method to the last emission of the library. + */ + private val sortTriggerRelay = BehaviorRelay.create(Unit) + + /** + * Library subscription. + */ + private var librarySubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + subscribeLibrary() + } + + /** + * Subscribes to library if needed. + */ + fun subscribeLibrary() { + if (librarySubscription.isNullOrUnsubscribed()) { + librarySubscription = getLibraryObservable() + .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> Pair(lib.first, applyFilters(lib.second)) }) + .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), + { lib, _ -> Pair(lib.first, applySort(lib.second)) }) + .map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache({ view, pair -> + view.onNextLibraryUpdate(pair.first, pair.second) + }) + } + } + + /** + * Applies library filters to the given map of manga. + * + * @param map the map to filter. + */ + private fun applyFilters(map: Map>): 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>> { + return db.getLibraryMangas().asRxObservable() + .map { list -> list.groupBy { it.category } } + } + + /** + * Requests the library to be filtered. + */ + fun requestFilterUpdate() { + filterTriggerRelay.call(Unit) + } + + /** + * Requests the library to be sorted. + */ + fun requestSortUpdate() { + sortTriggerRelay.call(Unit) + } + + /** + * Called when a manga is opened. + */ + fun onOpenManga() { + // Avoid further db updates for the library when it's not needed + librarySubscription?.let { remove(it) } + } + + /** + * Returns the common categories for the given list of manga. + * + * @param mangas the list of manga. + */ + fun getCommonCategories(mangas: List): Collection { + if (mangas.isEmpty()) return emptyList() + return mangas.toSet() + .map { db.getCategoriesForManga(it).executeAsBlocking() } + .reduce { set1: Iterable, set2 -> set1.intersect(set2) } + } + + /** + * Remove the selected manga from the library. + * + * @param mangas the list of manga to delete. + * @param deleteChapters whether to also delete downloaded chapters. + */ + fun removeMangaFromLibrary(mangas: List, deleteChapters: Boolean) { + // Create a set of the list + val mangaToDelete = mangas.distinctBy { it.id } + mangaToDelete.forEach { it.favorite = false } + + Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } + .onErrorResumeNext { Observable.empty() } + .subscribeOn(Schedulers.io()) + .subscribe() + + Observable.fromCallable { + mangaToDelete.forEach { manga -> + coverCache.deleteFromCache(manga.thumbnail_url) + if (deleteChapters) { + val source = sourceManager.get(manga.source) as? HttpSource + if (source != null) { + downloadManager.findMangaDir(source, manga)?.delete() + } + } + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Move the given list of manga to categories. + * + * @param categories the selected categories. + * @param mangas the list of manga to move. + */ + fun moveMangasToCategories(categories: List, mangas: List) { + val mc = ArrayList() + + for (manga in mangas) { + for (cat in categories) { + mc.add(MangaCategory.create(manga, cat)) + } + } + + db.setMangaCategories(mc, mangas) + } + + /** + * Update cover with local file. + * + * @param inputStream the new cover. + * @param manga the manga edited. + * @return true if the cover is updated, false otherwise + */ + @Throws(IOException::class) + fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { + if (manga.source == LocalSource.ID) { + LocalSource.updateCover(context, manga, inputStream) + return true + } + + if (manga.thumbnail_url != null && manga.favorite) { + coverCache.copyToCache(manga.thumbnail_url!!, inputStream) + return true + } + return false + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index e40a8144e8..a90a3b6c3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -1,160 +1,247 @@ -package eu.kanade.tachiyomi.ui.main - -import android.content.Intent -import android.os.Bundle -import android.support.v4.app.Fragment -import android.support.v4.app.TaskStackBuilder -import android.support.v4.view.GravityCompat -import android.view.MenuItem -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment -import eu.kanade.tachiyomi.ui.download.DownloadActivity -import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment -import eu.kanade.tachiyomi.ui.library.LibraryFragment -import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment -import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment -import eu.kanade.tachiyomi.ui.setting.SettingsActivity -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.toolbar.* -import uy.kohesive.injekt.injectLazy - -class MainActivity : BaseActivity() { - - val preferences: PreferencesHelper by injectLazy() - - private val startScreenId by lazy { - when (preferences.startScreen()) { - 1 -> R.id.nav_drawer_library - 2 -> R.id.nav_drawer_recently_read - 3 -> R.id.nav_drawer_recent_updates - else -> R.id.nav_drawer_library - } - } - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - - // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 - if (!isTaskRoot) { - finish() - return - } - - // Inflate activity_main.xml. - setContentView(R.layout.activity_main) - - // Handle Toolbar - setupToolbar(toolbar, backNavigation = false) - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp) - - // Set behavior of Navigation drawer - nav_view.setNavigationItemSelectedListener { item -> - // Make information view invisible - empty_view.hide() - - val id = item.itemId - - val oldFragment = supportFragmentManager.findFragmentById(R.id.frame_container) - if (oldFragment == null || oldFragment.tag.toInt() != id) { - when (id) { - R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id) - R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id) - R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) - R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id) - R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id) - R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java)) - R.id.nav_drawer_settings -> { - val intent = Intent(this, SettingsActivity::class.java) - startActivityForResult(intent, REQUEST_OPEN_SETTINGS) - } - } - } - drawer.closeDrawer(GravityCompat.START) - true - } - - if (savedState == null) { - // Set start screen - when (intent.action) { - SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) - SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) - SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) - SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) - else -> setSelectedDrawerItem(startScreenId) - } - - // Show changelog if needed - ChangelogDialogFragment.show(this, preferences, supportFragmentManager) - } - - - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> drawer.openDrawer(GravityCompat.START) - else -> return super.onOptionsItemSelected(item) - } - return true - } - - override fun onBackPressed() { - val fragment = supportFragmentManager.findFragmentById(R.id.frame_container) - if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { - drawer.closeDrawers() - } else if (fragment != null && fragment.tag.toInt() != startScreenId) { - if (resumed) { - setSelectedDrawerItem(startScreenId) - } - } else { - super.onBackPressed() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) { - if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) { - // If database is cleared avoid undefined behavior by recreating the stack. - TaskStackBuilder.create(this) - .addNextIntent(Intent(this, MainActivity::class.java)) - .startActivities() - } else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) { - // Delay activity recreation to avoid fragment leaks. - nav_view.post { recreate() } - } else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) { - nav_view.post { recreate() } - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun setSelectedDrawerItem(itemId: Int, triggerAction: Boolean = true) { - nav_view.setCheckedItem(itemId) - if (triggerAction) { - nav_view.menu.performIdentifierAction(itemId, 0) - } - } - - private fun setFragment(fragment: Fragment, itemId: Int) { - supportFragmentManager.beginTransaction() - .replace(R.id.frame_container, fragment, "$itemId") - .commit() - } - - fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) { - if (show) empty_view.show(drawable, textResource) else empty_view.hide() - } - - companion object { - private const val REQUEST_OPEN_SETTINGS = 200 - // Shortcut actions - private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" - private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" - private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" - private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" - } -} +package eu.kanade.tachiyomi.ui.main + +import android.animation.ObjectAnimator +import android.app.TaskStackBuilder +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.support.v4.view.GravityCompat +import android.support.v4.widget.DrawerLayout +import android.support.v7.graphics.drawable.DrawerArrowDrawable +import android.view.ViewGroup +import com.bluelinelabs.conductor.* +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.base.activity.BaseActivity +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController +import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController +import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.download.DownloadActivity +import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController +import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController +import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.ui.setting.SettingsActivity +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.toolbar.* +import uy.kohesive.injekt.injectLazy + + +class MainActivity : BaseActivity() { + + private lateinit var router: Router + + val preferences: PreferencesHelper by injectLazy() + + private var drawerArrow: DrawerArrowDrawable? = null + + private var secondaryDrawer: ViewGroup? = null + + private val startScreenId by lazy { + when (preferences.startScreen()) { + 1 -> R.id.nav_drawer_library + 2 -> R.id.nav_drawer_recently_read + 3 -> R.id.nav_drawer_recent_updates + else -> R.id.nav_drawer_library + } + } + + private val tabAnimator by lazy { TabsAnimator(tabs) } + + override fun onCreate(savedInstanceState: Bundle?) { + setAppTheme() + super.onCreate(savedInstanceState) + + // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 + if (!isTaskRoot) { + finish() + return + } + + setContentView(R.layout.activity_main) + + setSupportActionBar(toolbar) + + drawerArrow = DrawerArrowDrawable(this) + drawerArrow?.color = Color.WHITE + toolbar.navigationIcon = drawerArrow + + // Set behavior of Navigation drawer + nav_view.setNavigationItemSelectedListener { item -> + val id = item.itemId + + val currentRoot = router.backstack.firstOrNull() + if (currentRoot?.tag()?.toIntOrNull() != id) { + when (id) { + R.id.nav_drawer_library -> setRoot(LibraryController(), id) + R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) + R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id) + R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id) + R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id) + R.id.nav_drawer_downloads -> { + startActivity(Intent(this, DownloadActivity::class.java)) + } + R.id.nav_drawer_settings -> { + val intent = Intent(this, SettingsActivity::class.java) + startActivityForResult(intent, REQUEST_OPEN_SETTINGS) + } + } + } + drawer.closeDrawer(GravityCompat.START) + true + } + + val container = findViewById(R.id.controller_container) as ViewGroup + + router = Conductor.attachRouter(this, container, savedInstanceState) + if (!router.hasRootController()) { + // Set start screen + when (intent.action) { + SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) + SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) + SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) + SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) + SHORTCUT_MANGA -> router.setRoot( + RouterTransaction.with(MangaController(intent.extras))) + else -> setSelectedDrawerItem(startScreenId) + } + } + + toolbar.setNavigationOnClickListener { + if (router.backstackSize == 1) { + drawer.openDrawer(GravityCompat.START) + } else { + onBackPressed() + } + } + + router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener { + override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + syncActivityViewWithController(to, from) + } + + override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean, + container: ViewGroup, handler: ControllerChangeHandler) { + + } + + }) + + syncActivityViewWithController(router.backstack.lastOrNull()?.controller()) + + // TODO changelog controller + if (savedInstanceState == null) { + // Show changelog if needed + ChangelogDialogFragment.show(this, preferences, supportFragmentManager) + } + } + + override fun onDestroy() { + super.onDestroy() + nav_view?.setNavigationItemSelectedListener(null) + toolbar?.setNavigationOnClickListener(null) + } + + override fun onBackPressed() { + val backstackSize = router.backstackSize + if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { + drawer.closeDrawers() + } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { + setSelectedDrawerItem(startScreenId) + } else if (backstackSize == 1 || !router.handleBack()) { + super.onBackPressed() + } + } + + private fun setSelectedDrawerItem(itemId: Int) { + if (!isFinishing) { + nav_view.setCheckedItem(itemId) + nav_view.menu.performIdentifierAction(itemId, 0) + } + } + + private fun setRoot(controller: Controller, id: Int) { + router.setRoot(RouterTransaction.with(controller) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler()) + .tag(id.toString())) + } + + private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) { + if (from is DialogController || to is DialogController) { + return + } + + val showHamburger = router.backstackSize == 1 + if (showHamburger) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + } else { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + } + + ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start() + + if (from is TabbedController) { + from.cleanupTabs(tabs) + } + if (to is TabbedController) { + to.configureTabs(tabs) + tabAnimator.expand() + } else { + tabAnimator.collapse() + tabs.setupWithViewPager(null) + } + + if (from is SecondaryDrawerController) { + if (secondaryDrawer != null) { + from.cleanupSecondaryDrawer(drawer) + drawer.removeView(secondaryDrawer) + secondaryDrawer = null + } + } + if (to is SecondaryDrawerController) { + secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) } + } + + if (to is NoToolbarElevationController) { + appbar.disableElevation() + } else { + appbar.enableElevation() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) { + if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) { + // If database is cleared avoid undefined behavior by recreating the stack. + TaskStackBuilder.create(this) + .addNextIntent(Intent(this, MainActivity::class.java)) + .startActivities() + } else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) { + // Delay activity recreation to avoid fragment leaks. + nav_view.post { recreate() } + } else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) { + nav_view.post { recreate() } + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + companion object { + private const val REQUEST_OPEN_SETTINGS = 200 + // Shortcut actions + private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" + private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" + private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ" + private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" + const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt new file mode 100644 index 0000000000..82109cd326 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.main + +import android.support.design.widget.TabLayout +import android.view.animation.Animation +import android.view.animation.DecelerateInterpolator +import android.view.animation.Transformation +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.visible + +class TabsAnimator(val tabs: TabLayout) { + + private var height = 0 + + private val interpolator = DecelerateInterpolator() + + private val duration = 300L + + private val expandAnimation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + tabs.layoutParams.height = (height * interpolatedTime).toInt() + tabs.requestLayout() + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + private val collapseAnimation = object : Animation() { + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + if (interpolatedTime == 1f) { + tabs.gone() + } else { + tabs.layoutParams.height = (height * (1 - interpolatedTime)).toInt() + tabs.requestLayout() + } + } + + override fun willChangeBounds(): Boolean { + return true + } + } + + init { + collapseAnimation.duration = duration + collapseAnimation.interpolator = interpolator + expandAnimation.duration = duration + expandAnimation.interpolator = interpolator + } + + fun expand() { + tabs.visible() + if (measure() && tabs.measuredHeight != height) { + tabs.startAnimation(expandAnimation) + } + } + + fun collapse() { + if (measure() && tabs.measuredHeight != 0) { + tabs.startAnimation(collapseAnimation) + } else { + tabs.gone() + } + } + + /** + * Returns true if the view is measured, otherwise query dimensions and check again. + */ + private fun measure(): Boolean { + if (height > 0) return true + height = tabs.measuredHeight + return height > 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt deleted file mode 100644 index efada0dc35..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt +++ /dev/null @@ -1,141 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.support.graphics.drawable.VectorDrawableCompat -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentPagerAdapter -import android.widget.LinearLayout -import android.widget.TextView -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity -import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment -import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment -import eu.kanade.tachiyomi.ui.manga.track.TrackFragment -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.activity_manga.* -import kotlinx.android.synthetic.main.toolbar.* -import nucleus.factory.RequiresPresenter - -@RequiresPresenter(MangaPresenter::class) -class MangaActivity : BaseRxActivity() { - - companion object { - - const val FROM_CATALOGUE_EXTRA = "from_catalogue" - const val MANGA_EXTRA = "manga" - const val FROM_LAUNCHER_EXTRA = "from_launcher" - const val INFO_FRAGMENT = 0 - const val CHAPTERS_FRAGMENT = 1 - const val TRACK_FRAGMENT = 2 - - fun newIntent(context: Context, manga: Manga, fromCatalogue: Boolean = false): Intent { - SharedData.put(MangaEvent(manga)) - return Intent(context, MangaActivity::class.java).apply { - putExtra(FROM_CATALOGUE_EXTRA, fromCatalogue) - putExtra(MANGA_EXTRA, manga.id) - } - } - } - - private lateinit var adapter: MangaDetailAdapter - - var fromCatalogue: Boolean = false - private set - - override fun onCreate(savedState: Bundle?) { - setAppTheme() - super.onCreate(savedState) - setContentView(R.layout.activity_manga) - - val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false) - - // Remove any current manga if we are launching from launcher - if (fromLauncher) SharedData.remove(MangaEvent::class.java) - - presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) { - val id = intent.getLongExtra(MANGA_EXTRA, 0) - val dbManga = presenter.db.getManga(id).executeAsBlocking() - if (dbManga != null) { - MangaEvent(dbManga) - } else { - toast(R.string.manga_not_in_db) - finish() - return - } - }) - - setupToolbar(toolbar) - - fromCatalogue = intent.getBooleanExtra(FROM_CATALOGUE_EXTRA, false) - - adapter = MangaDetailAdapter(supportFragmentManager, this) - view_pager.offscreenPageLimit = 3 - view_pager.adapter = adapter - - tabs.setupWithViewPager(view_pager) - - if (!fromCatalogue) - view_pager.currentItem = CHAPTERS_FRAGMENT - - requestPermissionsOnMarshmallow() - } - - fun onSetManga(manga: Manga) { - setToolbarTitle(manga.title) - } - - fun setTrackingIcon(visible: Boolean) { - val tab = tabs.getTabAt(TRACK_FRAGMENT) ?: return - val drawable = if (visible) - VectorDrawableCompat.create(resources, R.drawable.ic_done_white_18dp, null) - else null - - // I had no choice but to use reflection... - val field = tab.javaClass.getDeclaredField("mView").apply { isAccessible = true } - val view = field.get(tab) as LinearLayout - val textView = view.getChildAt(1) as TextView - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) - textView.compoundDrawablePadding = 4 - } - - private class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) - : FragmentPagerAdapter(fm) { - - private var tabCount = 2 - - private val tabTitles = listOf( - R.string.manga_detail_tab, - R.string.manga_chapters_tab, - R.string.manga_tracking_tab) - .map { activity.getString(it) } - - init { - if (!activity.fromCatalogue && activity.presenter.trackManager.hasLoggedServices()) - tabCount++ - } - - override fun getCount(): Int { - return tabCount - } - - override fun getItem(position: Int): Fragment { - when (position) { - INFO_FRAGMENT -> return MangaInfoFragment.newInstance() - CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() - TRACK_FRAGMENT -> return TrackFragment.newInstance() - else -> throw Exception("Unknown position") - } - } - - override fun getPageTitle(position: Int): CharSequence { - return tabTitles[position] - } - - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt new file mode 100644 index 0000000000..4e3ad1e390 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -0,0 +1,186 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.Manifest.permission.READ_EXTERNAL_STORAGE +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.os.Build +import android.os.Bundle +import android.support.design.widget.TabLayout +import android.support.graphics.drawable.VectorDrawableCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType +import com.bluelinelabs.conductor.Router +import com.bluelinelabs.conductor.RouterTransaction +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.controller.RouterPagerAdapter +import eu.kanade.tachiyomi.ui.base.controller.RxController +import eu.kanade.tachiyomi.ui.base.controller.TabbedController +import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController +import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController +import eu.kanade.tachiyomi.ui.manga.track.TrackController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.manga_controller.view.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MangaController : RxController, TabbedController { + + constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply { + putLong(MANGA_EXTRA, manga?.id!!) + putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) + }) { + this.manga = manga + if (manga != null) { + source = Injekt.get().get(manga.source) + } + } + + constructor(mangaId: Long) : this( + Injekt.get().getManga(mangaId).executeAsBlocking()) + + @Suppress("unused") + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) + + var manga: Manga? = null + private set + + var source: Source? = null + private set + + private var adapter: MangaDetailAdapter? = null + + val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) + + val chapterCountRelay: BehaviorRelay = BehaviorRelay.create() + + val mangaFavoriteRelay: PublishRelay = PublishRelay.create() + + override fun getTitle(): String? { + return manga?.title + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.manga_controller, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + if (manga == null || source == null) return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(arrayOf(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE), 301) + } + + with(view) { + adapter = MangaDetailAdapter() + view_pager.offscreenPageLimit = 3 + view_pager.adapter = adapter + + if (!fromCatalogue) + view_pager.currentItem = CHAPTERS_CONTROLLER + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeStarted(handler, type) + if (type.isEnter) { + activity?.tabs?.setupWithViewPager(view?.view_pager) + } + } + + override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { + super.onChangeEnded(handler, type) + if (manga == null || source == null) { + activity?.toast(R.string.manga_not_in_db) + router.popController(this) + } + } + + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_FILL + tabMode = TabLayout.MODE_FIXED + } + } + + override fun cleanupTabs(tabs: TabLayout) { + setTrackingIcon(false) + } + + fun setTrackingIcon(visible: Boolean) { + val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return + val drawable = if (visible) + VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null) + else null + + // I had no choice but to use reflection... + val view = tabField.get(tab) as LinearLayout + val textView = view.getChildAt(1) as TextView + textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null) + textView.compoundDrawablePadding = if (visible) 4 else 0 + } + + private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) { + + private val tabCount = if (Injekt.get().hasLoggedServices()) 3 else 2 + + private val tabTitles = listOf( + R.string.manga_detail_tab, + R.string.manga_chapters_tab, + R.string.manga_tracking_tab) + .map { resources!!.getString(it) } + + override fun getCount(): Int { + return tabCount + } + + override fun configureRouter(router: Router, position: Int) { + if (!router.hasRootController()) { + val controller = when (position) { + INFO_CONTROLLER -> MangaInfoController() + CHAPTERS_CONTROLLER -> ChaptersController() + TRACK_CONTROLLER -> TrackController() + else -> error("Wrong position $position") + } + router.setRoot(RouterTransaction.with(controller)) + } + } + + override fun getPageTitle(position: Int): CharSequence { + return tabTitles[position] + } + + } + + companion object { + + const val FROM_CATALOGUE_EXTRA = "from_catalogue" + const val MANGA_EXTRA = "manga" + + const val INFO_CONTROLLER = 0 + const val CHAPTERS_CONTROLLER = 1 + const val TRACK_CONTROLLER = 2 + + private val tabField = TabLayout.Tab::class.java.getDeclaredField("mView") + .apply { isAccessible = true } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt deleted file mode 100644 index ff86676da5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import eu.kanade.tachiyomi.data.database.models.Manga - -class MangaEvent(val manga: Manga) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt deleted file mode 100644 index bf9edbc0ac..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent -import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import uy.kohesive.injekt.injectLazy - -/** - * Presenter of [MangaActivity]. - */ -class MangaPresenter : BasePresenter() { - - /** - * Database helper. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Tracking manager. - */ - val trackManager: TrackManager by injectLazy() - - /** - * Manga associated with this instance. - */ - lateinit var manga: Manga - - var mangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Prepare a subject to communicate the chapters and info presenters for the chapter count. - SharedData.put(ChapterCountEvent()) - // Prepare a subject to communicate the chapters and info presenters for the chapter favorite. - SharedData.put(MangaFavoriteEvent()) - } - - fun setMangaEvent(event: MangaEvent) { - if (mangaSubscription.isNullOrUnsubscribed()) { - manga = event.manga - mangaSubscription = Observable.just(manga) - .subscribeLatestCache(MangaActivity::onSetManga) - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt index 1b42ea2cfd..a7ce2ebfba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterHolder.kt @@ -6,23 +6,13 @@ import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.util.getResourceColor import kotlinx.android.synthetic.main.item_chapter.view.* -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols import java.util.* class ChapterHolder( private val view: View, - private val adapter: ChaptersAdapter) -: FlexibleViewHolder(view, adapter) { - - private val readColor = view.context.getResourceColor(android.R.attr.textColorHint) - private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary) - private val bookmarkedColor = view.context.getResourceColor(R.attr.colorAccent) - private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) - private val df = DateFormat.getDateInstance(DateFormat.SHORT) + private val adapter: ChaptersAdapter +) : FlexibleViewHolder(view, adapter) { init { // We need to post a Runnable to show the popup to make sure that the PopupMenu is @@ -36,19 +26,19 @@ class ChapterHolder( chapter_title.text = when (manga.displayMode) { Manga.DISPLAY_NUMBER -> { - val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) - context.getString(R.string.display_mode_chapter, formattedNumber) + val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) + context.getString(R.string.display_mode_chapter, number) } else -> chapter.name } // Set correct text color - chapter_title.setTextColor(if (chapter.read) readColor else unreadColor) - if (chapter.bookmark) chapter_title.setTextColor(bookmarkedColor) + chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) + if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor) if (chapter.date_upload > 0) { - chapter_date.text = df.format(Date(chapter.date_upload)) - chapter_date.setTextColor(if (chapter.read) readColor else unreadColor) + chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload)) + chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor) } else { chapter_date.text = "" } @@ -105,7 +95,7 @@ class ChapterHolder( // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> - adapter.menuItemListener(adapterPosition, menuItem) + adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem) true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt index 70f4a5dc18..12043826c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterItem.kt @@ -1,50 +1,57 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download - -class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), - Chapter by chapter { - - private var _status: Int = 0 - - var status: Int - get() = download?.status ?: _status - set(value) { _status = value } - - @Transient var download: Download? = null - - val isDownloaded: Boolean - get() = status == Download.DOWNLOADED - - override fun getLayoutRes(): Int { - return R.layout.item_chapter - } - - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder { - return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List?) { - holder.bind(this, manga) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other is ChapterItem) { - return chapter.id!! == other.chapter.id!! - } - return false - } - - override fun hashCode(): Int { - return chapter.id!!.hashCode() - } - +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download + +class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem(), + Chapter by chapter { + + private var _status: Int = 0 + + var status: Int + get() = download?.status ?: _status + set(value) { _status = value } + + @Transient var download: Download? = null + + val isDownloaded: Boolean + get() = status == Download.DOWNLOADED + + override fun getLayoutRes(): Int { + return R.layout.item_chapter + } + + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): ChapterHolder { + + return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: ChapterHolder, + position: Int, + payloads: List?) { + + holder.bind(this, manga) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is ChapterItem) { + return chapter.id!! == other.chapter.id!! + } + return false + } + + override fun hashCode(): Int { + return chapter.id!!.hashCode() + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt index 7f9cc21f14..ab72549e6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt @@ -1,19 +1,45 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.view.MenuItem -import eu.davidea.flexibleadapter.FlexibleAdapter - -class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter(null, fragment, true) { - - var items: List = emptyList() - - val menuItemListener: (Int, MenuItem) -> Unit = { position, item -> - fragment.onItemMenuClick(position, item) - } - - override fun updateDataSet(items: List) { - this.items = items - super.updateDataSet(items.toList()) - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.content.Context +import android.view.MenuItem +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols + +class ChaptersAdapter( + controller: ChaptersController, + context: Context +) : FlexibleAdapter(null, controller, true) { + + var items: List = emptyList() + + val menuItemListener: OnMenuItemClickListener = controller + + val readColor = context.getResourceColor(android.R.attr.textColorHint) + + val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary) + + val bookmarkedColor = context.getResourceColor(R.attr.colorAccent) + + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) + + override fun updateDataSet(items: List) { + this.items = items + super.updateDataSet(items.toList()) + } + + fun indexOf(item: ChapterItem): Int { + return items.indexOf(item) + } + + interface OnMenuItemClickListener { + fun onMenuItemClick(position: Int, item: MenuItem) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt new file mode 100644 index 0000000000..fa43e6e443 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt @@ -0,0 +1,470 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.view.clicks +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.getCoordinates +import eu.kanade.tachiyomi.util.snack +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.fragment_manga_chapters.view.* +import timber.log.Timber + +class ChaptersController : NucleusController(), + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ChaptersAdapter.OnMenuItemClickListener, + SetDisplayModeDialog.Listener, + SetSortingDialog.Listener, + DownloadChaptersDialog.Listener, + DeleteChaptersDialog.Listener { + + /** + * Adapter containing a list of chapters. + */ + private var adapter: ChaptersAdapter? = null + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Selected items. Used to restore selections after a rotation. + */ + private val selectedItems = mutableSetOf() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): ChaptersPresenter { + val ctrl = parentController as MangaController + return ChaptersPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.fragment_manga_chapters, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + // Init RecyclerView and adapter + adapter = ChaptersAdapter(this, view.context) + + with(view) { + recycler.adapter = adapter + recycler.layoutManager = LinearLayoutManager(context) + recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + recycler.setHasFixedSize(true) + // TODO enable in a future commit +// adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent)) +// adapter.toggleFastScroller() + + swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() } + + fab.clicks().subscribeUntilDestroy { + val item = presenter.getNextUnreadChapter() + if (item != null) { + // Create animation listener + val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?) { + openChapter(item.chapter, true) + } + } + + // Get coordinates and start animation + val coordinates = fab.getCoordinates() + if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { + openChapter(item.chapter) + } + } else { + context.toast(R.string.no_next_chapter) + } + } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + actionMode = null + } + + override fun onActivityResumed(activity: Activity) { + val view = view ?: return + + // Check if animation view is visible + if (view.reveal_view.visibility == View.VISIBLE) { + // Show the unReveal effect + val coordinates = view.fab.getCoordinates() + view.reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) + } + super.onActivityResumed(activity) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chapters, menu) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + // Initialize menu items. + val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return + val menuFilterUnread = menu.findItem(R.id.action_filter_unread) + val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) + val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) + + // Set correct checkbox values. + menuFilterRead.isChecked = presenter.onlyRead() + menuFilterUnread.isChecked = presenter.onlyUnread() + menuFilterDownloaded.isChecked = presenter.onlyDownloaded() + menuFilterBookmarked.isChecked = presenter.onlyBookmarked() + + if (presenter.onlyRead()) + //Disable unread filter option if read filter is enabled. + menuFilterUnread.isEnabled = false + if (presenter.onlyUnread()) + //Disable read filter option if unread filter is enabled. + menuFilterRead.isEnabled = false + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_display_mode -> showDisplayModeDialog() + R.id.manga_download -> showDownloadDialog() + R.id.action_sorting_mode -> showSortingDialog() + R.id.action_filter_unread -> { + item.isChecked = !item.isChecked + presenter.setUnreadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_read -> { + item.isChecked = !item.isChecked + presenter.setReadFilter(item.isChecked) + activity?.invalidateOptionsMenu() + } + R.id.action_filter_downloaded -> { + item.isChecked = !item.isChecked + presenter.setDownloadedFilter(item.isChecked) + } + R.id.action_filter_bookmarked -> { + item.isChecked = !item.isChecked + presenter.setBookmarkedFilter(item.isChecked) + } + R.id.action_filter_empty -> { + presenter.removeFilters() + activity?.invalidateOptionsMenu() + } + R.id.action_sort -> presenter.revertSortOrder() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + fun onNextChapters(chapters: List) { + // If the list is empty, fetch chapters from source if the conditions are met + // We use presenter chapters instead because they are always unfiltered + if (presenter.chapters.isEmpty()) + initialFetchChapters() + + val adapter = adapter ?: return + adapter.updateDataSet(chapters) + + if (selectedItems.isNotEmpty()) { + adapter.clearSelection() // we need to start from a clean state, index may have changed + createActionModeIfNeeded() + selectedItems.forEach { item -> + val position = adapter.indexOf(item) + if (position != -1 && !adapter.isSelected(position)) { + adapter.toggleSelection(position) + } + } + actionMode?.invalidate() + } + + } + + private fun initialFetchChapters() { + // Only fetch if this view is from the catalog and it hasn't requested previously + if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) { + fetchChaptersFromSource() + } + } + + fun fetchChaptersFromSource() { + view?.swipe_refresh?.isRefreshing = true + presenter.fetchChaptersFromSource() + } + + fun onFetchChaptersDone() { + view?.swipe_refresh?.isRefreshing = false + } + + fun onFetchChaptersError(error: Throwable) { + view?.swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChapterHolder? { + return view?.recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder + } + + fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) + if (hasAnimation) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + } + startActivity(intent) + } + + override fun onItemClick(position: Int): Boolean { + val adapter = adapter ?: return false + val item = adapter.getItem(position) ?: return false + if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item.chapter) + return false + } + } + + override fun onItemLongClick(position: Int) { + createActionModeIfNeeded() + toggleSelection(position) + } + + // SELECTIONS & ACTION MODE + + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + val item = adapter.getItem(position) ?: return + adapter.toggleSelection(position) + if (adapter.isSelected(position)) { + selectedItems.add(item) + } else { + selectedItems.remove(item) + } + actionMode?.invalidate() + } + + fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.map { adapter.getItem(it) } + } + + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) + } + } + + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_selection, menu) + adapter?.mode = FlexibleAdapter.MODE_MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_select_all -> selectAll() + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> showDeleteChaptersConfirmationDialog() + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter?.mode = FlexibleAdapter.MODE_SINGLE + adapter?.clearSelection() + selectedItems.clear() + actionMode = null + } + + override fun onMenuItemClick(position: Int, item: MenuItem) { + val chapter = adapter?.getItem(position) ?: return + val chapters = listOf(chapter) + + when (item.itemId) { + R.id.action_download -> downloadChapters(chapters) + R.id.action_bookmark -> bookmarkChapters(chapters, true) + R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) + R.id.action_delete -> deleteChapters(chapters) + R.id.action_mark_as_read -> markAsRead(chapters) + R.id.action_mark_as_unread -> markAsUnread(chapters) + R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter) + } + } + + // SELECTION MODE ACTIONS + + fun selectAll() { + val adapter = adapter ?: return + adapter.selectAll() + selectedItems.addAll(adapter.items) + actionMode?.invalidate() + } + + fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + } + + fun downloadChapters(chapters: List) { + val view = view + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + if (view != null && !presenter.manga.favorite) { + view.recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.action_add) { + presenter.addToLibrary() + } + } + } + } + + private fun showDeleteChaptersConfirmationDialog() { + DeleteChaptersDialog(this).showDialog(router) + } + + override fun deleteChapters() { + deleteChapters(getSelectedChapters()) + } + + fun markPreviousAsRead(chapter: ChapterItem) { + val adapter = adapter ?: return + val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items + val chapterPos = chapters.indexOf(chapter) + if (chapterPos != -1) { + presenter.markChaptersRead(chapters.take(chapterPos), true) + } + } + + fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + destroyActionModeIfNeeded() + presenter.bookmarkChapters(chapters, bookmarked) + } + + fun deleteChapters(chapters: List) { + destroyActionModeIfNeeded() + if (chapters.isEmpty()) return + + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chapters) + } + + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + // OVERFLOW MENU DIALOGS + + private fun showDisplayModeDialog() { + val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 + SetDisplayModeDialog(this, preselected).showDialog(router) + } + + override fun setDisplayMode(id: Int) { + presenter.setDisplayMode(id) + adapter?.notifyDataSetChanged() + } + + private fun showSortingDialog() { + val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 + SetSortingDialog(this, preselected).showDialog(router) + } + + override fun setSorting(id: Int) { + presenter.setSorting(id) + } + + private fun showDownloadDialog() { + DownloadChaptersDialog(this).showDialog(router) + } + + override fun downloadChapters(choice: Int) { + fun getUnreadChaptersSorted() = presenter.chapters + .filter { !it.read && it.status == Download.NOT_DOWNLOADED } + .distinctBy { it.name } + .sortedByDescending { it.source_order } + + // i = 0: Download 1 + // i = 1: Download 5 + // i = 2: Download 10 + // i = 3: Download unread + // i = 4: Download all + val chaptersToDownload = when (choice) { + 0 -> getUnreadChaptersSorted().take(1) + 1 -> getUnreadChaptersSorted().take(5) + 2 -> getUnreadChaptersSorted().take(10) + 3 -> presenter.chapters.filter { !it.read } + 4 -> presenter.chapters + else -> emptyList() + } + + if (chaptersToDownload.isNotEmpty()) { + downloadChapters(chaptersToDownload) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt deleted file mode 100644 index 2627e165c8..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt +++ /dev/null @@ -1,454 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Intent -import android.os.Bundle -import android.support.design.widget.Snackbar -import android.support.v4.app.DialogFragment -import android.support.v7.app.AppCompatActivity -import android.support.v7.view.ActionMode -import android.support.v7.widget.DividerItemDecoration -import android.support.v7.widget.LinearLayoutManager -import android.view.* -import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.getCoordinates -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.DeletingChaptersDialog -import kotlinx.android.synthetic.main.fragment_manga_chapters.* -import nucleus.factory.RequiresPresenter -import timber.log.Timber - -@RequiresPresenter(ChaptersPresenter::class) -class ChaptersFragment : BaseRxFragment(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener { - - companion object { - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [ChaptersFragment]. - */ - fun newInstance(): ChaptersFragment { - return ChaptersFragment() - } - - } - - /** - * Adapter containing a list of chapters. - */ - private lateinit var adapter: ChaptersAdapter - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_manga_chapters, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - // Init RecyclerView and adapter - adapter = ChaptersAdapter(this) - - recycler.adapter = adapter - recycler.layoutManager = LinearLayoutManager(activity) - recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) -// TODO enable in a future commit -// adapter.setFastScroller(fast_scroller, context.getResourceColor(R.attr.colorAccent)) -// adapter.toggleFastScroller() - - swipe_refresh.setOnRefreshListener { fetchChapters() } - - fab.setOnClickListener { - val item = presenter.getNextUnreadChapter() - if (item != null) { - // Create animation listener - val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { - openChapter(item.chapter, true) - } - } - - // Get coordinates and start animation - val coordinates = fab.getCoordinates() - if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) { - openChapter(item.chapter) - } - } else { - context.toast(R.string.no_next_chapter) - } - } - } - - override fun onResume() { - // Check if animation view is visible - if (reveal_view.visibility == View.VISIBLE) { - // Show the unReveal effect - val coordinates = fab.getCoordinates() - reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - super.onResume() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.chapters, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize menu items. - val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return - val menuFilterUnread = menu.findItem(R.id.action_filter_unread) - val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded) - val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked) - - // Set correct checkbox values. - menuFilterRead.isChecked = presenter.onlyRead() - menuFilterUnread.isChecked = presenter.onlyUnread() - menuFilterDownloaded.isChecked = presenter.onlyDownloaded() - menuFilterBookmarked.isChecked = presenter.onlyBookmarked() - - if (presenter.onlyRead()) - //Disable unread filter option if read filter is enabled. - menuFilterUnread.isEnabled = false - if (presenter.onlyUnread()) - //Disable read filter option if unread filter is enabled. - menuFilterRead.isEnabled = false - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_display_mode -> showDisplayModeDialog() - R.id.manga_download -> showDownloadDialog() - R.id.action_sorting_mode -> showSortingDialog() - R.id.action_filter_unread -> { - item.isChecked = !item.isChecked - presenter.setUnreadFilter(item.isChecked) - activity.supportInvalidateOptionsMenu() - } - R.id.action_filter_read -> { - item.isChecked = !item.isChecked - presenter.setReadFilter(item.isChecked) - activity.supportInvalidateOptionsMenu() - } - R.id.action_filter_downloaded -> { - item.isChecked = !item.isChecked - presenter.setDownloadedFilter(item.isChecked) - } - R.id.action_filter_bookmarked -> { - item.isChecked = !item.isChecked - presenter.setBookmarkedFilter(item.isChecked) - } - R.id.action_filter_empty -> { - presenter.removeFilters() - activity.supportInvalidateOptionsMenu() - } - R.id.action_sort -> presenter.revertSortOrder() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - @Suppress("UNUSED_PARAMETER") - fun onNextManga(manga: Manga) { - // Set initial values - activity.supportInvalidateOptionsMenu() - } - - fun onNextChapters(chapters: List) { - // If the list is empty, fetch chapters from source if the conditions are met - // We use presenter chapters instead because they are always unfiltered - if (presenter.chapters.isEmpty()) - initialFetchChapters() - - destroyActionModeIfNeeded() - adapter.updateDataSet(chapters) - } - - private fun initialFetchChapters() { - // Only fetch if this view is from the catalog and it hasn't requested previously - if (isCatalogueManga && !presenter.hasRequested) { - fetchChapters() - } - } - - fun fetchChapters() { - swipe_refresh.isRefreshing = true - presenter.fetchChaptersFromSource() - } - - fun onFetchChaptersDone() { - swipe_refresh.isRefreshing = false - } - - fun onFetchChaptersError(error: Throwable) { - swipe_refresh.isRefreshing = false - context.toast(error.message) - } - - val isCatalogueManga: Boolean - get() = (activity as MangaActivity).fromCatalogue - - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { - val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) - } - startActivity(intent) - } - - private fun showDisplayModeDialog() { - // Get available modes, ids and the selected mode - val modes = intArrayOf(R.string.show_title, R.string.show_chapter_number) - val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) - val selectedIndex = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 - - MaterialDialog.Builder(activity) - .title(R.string.action_display_mode) - .items(modes.map { getString(it) }) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - // Save the new display mode - presenter.setDisplayMode(itemView.id) - // Refresh ui - adapter.notifyItemRangeChanged(0, adapter.itemCount) - true - } - .show() - } - - private fun showSortingDialog() { - // Get available modes, ids and the selected mode - val modes = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) - val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) - val selectedIndex = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1 - - MaterialDialog.Builder(activity) - .title(R.string.sorting_mode) - .items(modes.map { getString(it) }) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> - // Save the new sorting mode - presenter.setSorting(itemView.id) - true - } - .show() - } - - private fun showDownloadDialog() { - // Get available modes - val modes = intArrayOf(R.string.download_1, R.string.download_5, R.string.download_10, - R.string.download_unread, R.string.download_all) - - MaterialDialog.Builder(activity) - .title(R.string.manga_download) - .negativeText(android.R.string.cancel) - .items(modes.map { getString(it) }) - .itemsCallback { _, _, i, _ -> - - fun getUnreadChaptersSorted() = presenter.chapters - .filter { !it.read && it.status == Download.NOT_DOWNLOADED } - .distinctBy { it.name } - .sortedByDescending { it.source_order } - - // i = 0: Download 1 - // i = 1: Download 5 - // i = 2: Download 10 - // i = 3: Download unread - // i = 4: Download all - val chaptersToDownload = when (i) { - 0 -> getUnreadChaptersSorted().take(1) - 1 -> getUnreadChaptersSorted().take(5) - 2 -> getUnreadChaptersSorted().take(10) - 3 -> presenter.chapters.filter { !it.read } - 4 -> presenter.chapters - else -> emptyList() - } - - if (chaptersToDownload.isNotEmpty()) { - downloadChapters(chaptersToDownload) - } - } - .show() - } - - fun onChapterStatusChange(download: Download) { - getHolder(download.chapter)?.notifyStatus(download.status) - } - - private fun getHolder(chapter: Chapter): ChapterHolder? { - return recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_selection, menu) - adapter.mode = FlexibleAdapter.MODE_MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - return false - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_select_all -> selectAll() - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> { - MaterialDialog.Builder(activity) - .content(R.string.confirm_delete_chapters) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { _, _ -> deleteChapters(getSelectedChapters()) } - .show() - } - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - adapter.mode = FlexibleAdapter.MODE_SINGLE - adapter.clearSelection() - actionMode = null - } - - fun getSelectedChapters(): List { - return adapter.selectedPositions.map { adapter.getItem(it) } - } - - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - fun selectAll() { - adapter.selectAll() - setContextTitle(adapter.selectedItemCount) - } - - fun markAsRead(chapters: List) { - presenter.markChaptersRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - fun markAsUnread(chapters: List) { - presenter.markChaptersRead(chapters, false) - } - - fun markPreviousAsRead(chapter: ChapterItem) { - val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items - val chapterPos = chapters.indexOf(chapter) - if (chapterPos != -1) { - presenter.markChaptersRead(chapters.take(chapterPos), true) - } - } - - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - if (!presenter.manga.favorite){ - recycler.snack(getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.action_add) { - presenter.addToLibrary() - } - } - } - } - - fun bookmarkChapters(chapters: List, bookmarked: Boolean) { - destroyActionModeIfNeeded() - presenter.bookmarkChapters(chapters, bookmarked) - } - - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(chapters) - } - - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter.notifyItemRangeChanged(0, adapter.itemCount) - } - - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - fun dismissDeletingDialog() { - (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment) - ?.dismissAllowingStateLoss() - } - - override fun onItemClick(position: Int): Boolean { - val item = adapter.getItem(position) ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item.chapter) - return false - } - } - - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - fun onItemMenuClick(position: Int, item: MenuItem) { - val chapter = adapter.getItem(position)?.let { listOf(it) } ?: return - - when (item.itemId) { - R.id.action_download -> downloadChapters(chapter) - R.id.action_bookmark -> bookmarkChapters(chapter, true) - R.id.action_remove_bookmark -> bookmarkChapters(chapter, false) - R.id.action_delete -> deleteChapters(chapter) - R.id.action_mark_as_read -> markAsRead(chapter) - R.id.action_mark_as_unread -> markAsUnread(chapter) - R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter[0]) - } - } - - private fun toggleSelection(position: Int) { - adapter.toggleSelection(position) - - val count = adapter.selectedItemCount - if (count == 0) { - actionMode?.finish() - } else { - setContextTitle(count) - actionMode?.invalidate() - } - } - - private fun setContextTitle(count: Int) { - actionMode?.title = getString(R.string.label_selected, count) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index be294a97e0..254b9f9917 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -1,446 +1,415 @@ -package eu.kanade.tachiyomi.ui.manga.chapter - -import android.os.Bundle -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent -import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import eu.kanade.tachiyomi.util.syncChaptersWithSource -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import timber.log.Timber -import uy.kohesive.injekt.injectLazy - -/** - * Presenter of [ChaptersFragment]. - */ -class ChaptersPresenter : BasePresenter() { - - /** - * Database helper. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Source manager. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Preferences. - */ - val preferences: PreferencesHelper by injectLazy() - - /** - * Downloads manager. - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Active manga. - */ - lateinit var manga: Manga - private set - - /** - * Source of the manga. - */ - lateinit var source: Source - private set - - /** - * List of chapters of the manga. It's always unfiltered and unsorted. - */ - var chapters: List = emptyList() - private set - - /** - * Subject of list of chapters to allow updating the view without going to DB. - */ - val chaptersRelay: PublishRelay> - by lazy { PublishRelay.create>() } - - /** - * Whether the chapter list has been requested to the source. - */ - var hasRequested = false - private set - - /** - * Subscription to retrieve the new list of chapters from the source. - */ - private var fetchChaptersSubscription: Subscription? = null - - /** - * Subscription to observe download status changes. - */ - private var observeDownloadsSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - // Find the active manga from the shared data or return. - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - source = sourceManager.get(manga.source)!! - Observable.just(manga) - .subscribeLatestCache(ChaptersFragment::onNextManga) - - // Prepare the relay. - chaptersRelay.flatMap { applyChapterFilters(it) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(ChaptersFragment::onNextChapters, - { _, error -> Timber.e(error) }) - - // Add the subscription that retrieves the chapters from the database, keeps subscribed to - // changes, and sends the list of chapters to the relay. - add(db.getChapters(manga).asRxObservable() - .map { chapters -> - // Convert every chapter to a model. - chapters.map { it.toModel() } - } - .doOnNext { chapters -> - // Find downloaded chapters - setDownloadedChapters(chapters) - - // Store the last emission - this.chapters = chapters - - // Listen for download status changes - observeDownloads() - - // Emit the number of chapters to the info tab. - SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size) - } - .subscribe { chaptersRelay.call(it) }) - } - - private fun observeDownloads() { - observeDownloadsSubscription?.let { remove(it) } - observeDownloadsSubscription = downloadManager.queue.getStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } - .doOnNext { onDownloadStatusChange(it) } - .subscribeLatestCache(ChaptersFragment::onChapterStatusChange, - { _, error -> Timber.e(error) }) - } - - /** - * Converts a chapter from the database to an extended model, allowing to store new fields. - */ - private fun Chapter.toModel(): ChapterItem { - // Create the model object. - val model = ChapterItem(this, manga) - - // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } - - if (download != null) { - // If there's an active download, assign it. - model.download = download - } - return model - } - - /** - * Finds and assigns the list of downloaded chapters. - * - * @param chapters the list of chapter from the database. - */ - private fun setDownloadedChapters(chapters: List) { - val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return - val cached = mutableMapOf() - files.mapNotNull { it.name } - .mapNotNull { name -> chapters.find { - name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) } - } } - .forEach { it.status = Download.DOWNLOADED } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - - if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onFetchChaptersDone() - }, ChaptersFragment::onFetchChaptersError) - } - - /** - * Updates the UI after applying the filters. - */ - private fun refreshChapters() { - chaptersRelay.call(chapters) - } - - /** - * Applies the view filters to the list of chapters obtained from the database. - * @param chapters the list of chapters from the database - * @return an observable of the list of chapters filtered and sorted. - */ - private fun applyChapterFilters(chapters: List): Observable> { - var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) - if (onlyUnread()) { - observable = observable.filter { !it.read } - } - else if (onlyRead()) { - observable = observable.filter { it.read } - } - if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded } - } - if (onlyBookmarked()) { - observable = observable.filter { it.bookmark } - } - val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { - Manga.SORTING_SOURCE -> when (sortDescending()) { - true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } - false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } - } - Manga.SORTING_NUMBER -> when (sortDescending()) { - true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } - false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } - } - else -> throw NotImplementedError("Unimplemented sorting method") - } - return observable.toSortedList(sortFunction) - } - - /** - * Called when a download for the active manga changes status. - * @param download the download whose status changed. - */ - fun onDownloadStatusChange(download: Download) { - // Assign the download to the model object. - if (download.status == Download.QUEUE) { - chapters.find { it.id == download.chapter.id }?.let { - if (it.download == null) { - it.download = download - } - } - } - - // Force UI update if downloaded filter active and download finished. - if (onlyDownloaded() && download.status == Download.DOWNLOADED) - refreshChapters() - } - - /** - * Returns the next unread chapter or null if everything is read. - */ - fun getNextUnreadChapter(): ChapterItem? { - return chapters.sortedByDescending { it.source_order }.find { !it.read } - } - - /** - * Mark the selected chapter list as read/unread. - * @param selectedChapters the list of selected chapters. - * @param read whether to mark chapters as read or unread. - */ - fun markChaptersRead(selectedChapters: List, read: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.read = read - if (!read) { - chapter.last_page_read = 0 - } - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Downloads the given list of chapters with the manager. - * @param chapters the list of chapters to download. - */ - fun downloadChapters(chapters: List) { - DownloadService.start(context) - downloadManager.downloadChapters(manga, chapters) - } - - /** - * Bookmarks the given list of chapters. - * @param selectedChapters the list of chapters to bookmark. - */ - fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { - Observable.from(selectedChapters) - .doOnNext { chapter -> - chapter.bookmark = bookmarked - } - .toList() - .flatMap { db.updateChaptersProgress(it).asRxObservable() } - .subscribeOn(Schedulers.io()) - .subscribe() - } - - /** - * Deletes the given list of chapter. - * @param chapters the list of chapters to delete. - */ - fun deleteChapters(chapters: List) { - Observable.from(chapters) - .doOnNext { deleteChapter(it) } - .toList() - .doOnNext { if (onlyDownloaded()) refreshChapters() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onChaptersDeleted() - }, ChaptersFragment::onChaptersDeletedError) - } - - /** - * Deletes a chapter from disk. This method is called in a background thread. - * @param chapter the chapter to delete. - */ - private fun deleteChapter(chapter: ChapterItem) { - downloadManager.queue.remove(chapter) - downloadManager.deleteChapter(source, manga, chapter) - chapter.status = Download.NOT_DOWNLOADED - chapter.download = null - } - - /** - * Reverses the sorting and requests an UI update. - */ - fun revertSortOrder() { - manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyUnread whether to display only unread chapters or all chapters. - */ - fun setUnreadFilter(onlyUnread: Boolean) { - manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the read filter and requests an UI update. - * @param onlyRead whether to display only read chapters or all chapters. - */ - fun setReadFilter(onlyRead: Boolean) { - manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the download filter and requests an UI update. - * @param onlyDownloaded whether to display only downloaded chapters or all chapters. - */ - fun setDownloadedFilter(onlyDownloaded: Boolean) { - manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Sets the bookmark filter and requests an UI update. - * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. - */ - fun setBookmarkedFilter(onlyBookmarked: Boolean) { - manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Removes all filters and requests an UI update. - */ - fun removeFilters() { - manga.readFilter = Manga.SHOW_ALL - manga.downloadedFilter = Manga.SHOW_ALL - manga.bookmarkedFilter = Manga.SHOW_ALL - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Adds manga to library - */ - fun addToLibrary() { - SharedData.get(MangaFavoriteEvent::class.java)?.call(true) - } - - /** - * Sets the active display mode. - * @param mode the mode to set. - */ - fun setDisplayMode(mode: Int) { - manga.displayMode = mode - db.updateFlags(manga).executeAsBlocking() - } - - /** - * Sets the sorting method and requests an UI update. - * @param sort the sorting mode. - */ - fun setSorting(sort: Int) { - manga.sorting = sort - db.updateFlags(manga).executeAsBlocking() - refreshChapters() - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyDownloaded(): Boolean { - return manga.downloadedFilter == Manga.SHOW_DOWNLOADED - } - - /** - * Whether the display only downloaded filter is enabled. - */ - fun onlyBookmarked(): Boolean { - return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED - } - - /** - * Whether the display only unread filter is enabled. - */ - fun onlyUnread(): Boolean { - return manga.readFilter == Manga.SHOW_UNREAD - } - - /** - * Whether the display only read filter is enabled. - */ - fun onlyRead(): Boolean { - return manga.readFilter == Manga.SHOW_READ - } - - /** - * Whether the sorting method is descending or ascending. - */ - fun sortDescending(): Boolean { - return manga.sortDescending() - } - -} +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [ChaptersController]. + */ +class ChaptersPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + /** + * List of chapters of the manga. It's always unfiltered and unsorted. + */ + var chapters: List = emptyList() + private set + + /** + * Subject of list of chapters to allow updating the view without going to DB. + */ + val chaptersRelay: PublishRelay> + by lazy { PublishRelay.create>() } + + /** + * Whether the chapter list has been requested to the source. + */ + var hasRequested = false + private set + + /** + * Subscription to retrieve the new list of chapters from the source. + */ + private var fetchChaptersSubscription: Subscription? = null + + /** + * Subscription to observe download status changes. + */ + private var observeDownloadsSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Prepare the relay. + chaptersRelay.flatMap { applyChapterFilters(it) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(ChaptersController::onNextChapters, + { _, error -> Timber.e(error) }) + + // Add the subscription that retrieves the chapters from the database, keeps subscribed to + // changes, and sends the list of chapters to the relay. + add(db.getChapters(manga).asRxObservable() + .map { chapters -> + // Convert every chapter to a model. + chapters.map { it.toModel() } + } + .doOnNext { chapters -> + // Find downloaded chapters + setDownloadedChapters(chapters) + + // Store the last emission + this.chapters = chapters + + // Listen for download status changes + observeDownloads() + + // Emit the number of chapters to the info tab. + chapterCountRelay.call(chapters.size) + } + .subscribe { chaptersRelay.call(it) }) + } + + private fun observeDownloads() { + observeDownloadsSubscription?.let { remove(it) } + observeDownloadsSubscription = downloadManager.queue.getStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .filter { download -> download.manga.id == manga.id } + .doOnNext { onDownloadStatusChange(it) } + .subscribeLatestCache(ChaptersController::onChapterStatusChange, + { _, error -> Timber.e(error) }) + } + + /** + * Converts a chapter from the database to an extended model, allowing to store new fields. + */ + private fun Chapter.toModel(): ChapterItem { + // Create the model object. + val model = ChapterItem(this, manga) + + // Find an active download for this chapter. + val download = downloadManager.queue.find { it.chapter.id == id } + + if (download != null) { + // If there's an active download, assign it. + model.download = download + } + return model + } + + /** + * Finds and assigns the list of downloaded chapters. + * + * @param chapters the list of chapter from the database. + */ + private fun setDownloadedChapters(chapters: List) { + val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return + val cached = mutableMapOf() + files.mapNotNull { it.name } + .mapNotNull { name -> chapters.find { + name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) } + } } + .forEach { it.status = Download.DOWNLOADED } + } + + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource() { + hasRequested = true + + if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return + fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } + .subscribeOn(Schedulers.io()) + .map { syncChaptersWithSource(db, it, manga, source) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onFetchChaptersDone() + }, ChaptersController::onFetchChaptersError) + } + + /** + * Updates the UI after applying the filters. + */ + private fun refreshChapters() { + chaptersRelay.call(chapters) + } + + /** + * Applies the view filters to the list of chapters obtained from the database. + * @param chapters the list of chapters from the database + * @return an observable of the list of chapters filtered and sorted. + */ + private fun applyChapterFilters(chapters: List): Observable> { + var observable = Observable.from(chapters).subscribeOn(Schedulers.io()) + if (onlyUnread()) { + observable = observable.filter { !it.read } + } + else if (onlyRead()) { + observable = observable.filter { it.read } + } + if (onlyDownloaded()) { + observable = observable.filter { it.isDownloaded } + } + if (onlyBookmarked()) { + observable = observable.filter { it.bookmark } + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.SORTING_SOURCE -> when (sortDescending()) { + true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } + false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } + } + Manga.SORTING_NUMBER -> when (sortDescending()) { + true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } + false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } + } + else -> throw NotImplementedError("Unimplemented sorting method") + } + return observable.toSortedList(sortFunction) + } + + /** + * Called when a download for the active manga changes status. + * @param download the download whose status changed. + */ + fun onDownloadStatusChange(download: Download) { + // Assign the download to the model object. + if (download.status == Download.QUEUE) { + chapters.find { it.id == download.chapter.id }?.let { + if (it.download == null) { + it.download = download + } + } + } + + // Force UI update if downloaded filter active and download finished. + if (onlyDownloaded() && download.status == Download.DOWNLOADED) + refreshChapters() + } + + /** + * Returns the next unread chapter or null if everything is read. + */ + fun getNextUnreadChapter(): ChapterItem? { + return chapters.sortedByDescending { it.source_order }.find { !it.read } + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChaptersRead(selectedChapters: List, read: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.read = read + if (!read) { + chapter.last_page_read = 0 + } + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Downloads the given list of chapters with the manager. + * @param chapters the list of chapters to download. + */ + fun downloadChapters(chapters: List) { + DownloadService.start(context) + downloadManager.downloadChapters(manga, chapters) + } + + /** + * Bookmarks the given list of chapters. + * @param selectedChapters the list of chapters to bookmark. + */ + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + Observable.from(selectedChapters) + .doOnNext { chapter -> + chapter.bookmark = bookmarked + } + .toList() + .flatMap { db.updateChaptersProgress(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + /** + * Deletes the given list of chapter. + * @param chapters the list of chapters to delete. + */ + fun deleteChapters(chapters: List) { + Observable.from(chapters) + .doOnNext { deleteChapter(it) } + .toList() + .doOnNext { if (onlyDownloaded()) refreshChapters() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, _ -> + view.onChaptersDeleted() + }, ChaptersController::onChaptersDeletedError) + } + + /** + * Deletes a chapter from disk. This method is called in a background thread. + * @param chapter the chapter to delete. + */ + private fun deleteChapter(chapter: ChapterItem) { + downloadManager.queue.remove(chapter) + downloadManager.deleteChapter(source, manga, chapter) + chapter.status = Download.NOT_DOWNLOADED + chapter.download = null + } + + /** + * Reverses the sorting and requests an UI update. + */ + fun revertSortOrder() { + manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC) + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyUnread whether to display only unread chapters or all chapters. + */ + fun setUnreadFilter(onlyUnread: Boolean) { + manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the read filter and requests an UI update. + * @param onlyRead whether to display only read chapters or all chapters. + */ + fun setReadFilter(onlyRead: Boolean) { + manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the download filter and requests an UI update. + * @param onlyDownloaded whether to display only downloaded chapters or all chapters. + */ + fun setDownloadedFilter(onlyDownloaded: Boolean) { + manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Sets the bookmark filter and requests an UI update. + * @param onlyBookmarked whether to display only bookmarked chapters or all chapters. + */ + fun setBookmarkedFilter(onlyBookmarked: Boolean) { + manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Removes all filters and requests an UI update. + */ + fun removeFilters() { + manga.readFilter = Manga.SHOW_ALL + manga.downloadedFilter = Manga.SHOW_ALL + manga.bookmarkedFilter = Manga.SHOW_ALL + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Adds manga to library + */ + fun addToLibrary() { + mangaFavoriteRelay.call(true) + } + + /** + * Sets the active display mode. + * @param mode the mode to set. + */ + fun setDisplayMode(mode: Int) { + manga.displayMode = mode + db.updateFlags(manga).executeAsBlocking() + } + + /** + * Sets the sorting method and requests an UI update. + * @param sort the sorting mode. + */ + fun setSorting(sort: Int) { + manga.sorting = sort + db.updateFlags(manga).executeAsBlocking() + refreshChapters() + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyDownloaded(): Boolean { + return manga.downloadedFilter == Manga.SHOW_DOWNLOADED + } + + /** + * Whether the display only downloaded filter is enabled. + */ + fun onlyBookmarked(): Boolean { + return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED + } + + /** + * Whether the display only unread filter is enabled. + */ + fun onlyUnread(): Boolean { + return manga.readFilter == Manga.SHOW_UNREAD + } + + /** + * Whether the display only read filter is enabled. + */ + fun onlyRead(): Boolean { + return manga.readFilter == Manga.SHOW_READ + } + + /** + * Whether the sorting method is descending or ascending. + */ + fun sortDescending(): Boolean { + return manga.sortDescending() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt new file mode 100644 index 0000000000..a269fe0853 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeleteChaptersDialog.kt @@ -0,0 +1,32 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DeleteChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.confirm_delete_chapters) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? Listener)?.deleteChapters() + } + .show() + } + + interface Listener { + fun deleteChapters() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt new file mode 100644 index 0000000000..fcfd6b9ade --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DeletingChaptersDialog.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Router +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { + + companion object { + const val TAG = "deleting_dialog" + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.deleting) + .build() + } + + override fun showDialog(router: Router) { + showDialog(router, TAG) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt new file mode 100644 index 0000000000..c54797a1f9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/DownloadChaptersDialog.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DownloadChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : DownloadChaptersDialog.Listener { + + constructor(target: T) : this() { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + val choices = intArrayOf( + R.string.download_1, + R.string.download_5, + R.string.download_10, + R.string.download_unread, + R.string.download_all + ).map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.manga_download) + .negativeText(android.R.string.cancel) + .items(choices) + .itemsCallback { _, _, position, _ -> + (targetController as? Listener)?.downloadChapters(position) + } + .build() + } + + interface Listener { + fun downloadChapters(choice: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt new file mode 100644 index 0000000000..608742b748 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetDisplayModeDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetDisplayModeDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetDisplayModeDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) + val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.action_display_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setDisplayMode(itemView.id) + true + } + .build() + } + + interface Listener { + fun setDisplayMode(id: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt new file mode 100644 index 0000000000..c6baca5b9a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/SetSortingDialog.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class SetSortingDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : SetSortingDialog.Listener { + + private val selectedIndex = args.getInt("selected", -1) + + constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply { + putInt("selected", selectedIndex) + }) { + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER) + val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number) + .map { activity.getString(it) } + + return MaterialDialog.Builder(activity) + .title(R.string.sorting_mode) + .items(choices) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ -> + (targetController as? Listener)?.setSorting(itemView.id) + true + } + .build() + } + + interface Listener { + fun setSorting(id: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt deleted file mode 100644 index d307941bcf..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/ChapterCountEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import rx.Observable -import rx.subjects.BehaviorSubject - -class ChapterCountEvent { - - private val subject = BehaviorSubject.create() - - val observable: Observable - get() = subject - - fun emit(count: Int) { - subject.onNext(count) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt deleted file mode 100644 index 75beb742c7..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFavoriteEvent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import com.jakewharton.rxrelay.PublishRelay -import rx.Observable - -class MangaFavoriteEvent { - - private val subject = PublishRelay.create() - - val observable: Observable - get() = subject - - fun call(favorite: Boolean) { - subject.call(favorite) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt new file mode 100644 index 0000000000..eb7a5553c2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -0,0 +1,399 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.support.customtabs.CustomTabsIntent +import android.view.* +import com.afollestad.materialdialogs.MaterialDialog +import com.bumptech.glide.BitmapRequestBuilder +import com.bumptech.glide.BitmapTypeRequest +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.view.clicks +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.snack +import eu.kanade.tachiyomi.util.toast +import jp.wasabeef.glide.transformations.CropCircleTransformation +import jp.wasabeef.glide.transformations.CropSquareTransformation +import jp.wasabeef.glide.transformations.MaskTransformation +import jp.wasabeef.glide.transformations.RoundedCornersTransformation +import kotlinx.android.synthetic.main.fragment_manga_info.view.* +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import rx.subscriptions.Subscriptions +import uy.kohesive.injekt.injectLazy + +/** + * Fragment that shows manga information. + * Uses R.layout.fragment_manga_info. + * UI related actions should be called from here. + */ +class MangaInfoController : NucleusController(), + ChangeMangaCategoriesDialog.Listener { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + init { + setHasOptionsMenu(true) + setOptionsMenuHidden(true) + } + + override fun createPresenter(): MangaInfoPresenter { + val ctrl = parentController as MangaController + return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, + ctrl.chapterCountRelay, ctrl.mangaFavoriteRelay) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.fragment_manga_info, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Set onclickListener to toggle favorite when FAB clicked. + fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } + + // Set SwipeRefresh to refresh manga data. + swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } + } + + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.manga_info, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_open_in_browser -> openInBrowser() + R.id.action_share -> shareManga() + R.id.action_add_to_home_screen -> addToHomeScreen() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Check if manga is initialized. + * If true update view with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextManga(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + setMangaInfo(manga, source) + } else { + // Initialize manga. + fetchMangaFromSource() + } + } + + /** + * Update the view with manga information. + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + private fun setMangaInfo(manga: Manga, source: Source?) { + val view = view ?: return + with(view) { + // Update artist TextView. + manga_artist.text = manga.artist + + // Update author TextView. + manga_author.text = manga.author + + // If manga source is known update source TextView. + if (source != null) { + manga_source.text = source.toString() + } + + // Update genres TextView. + manga_genres.text = manga.genre + + // Update status TextView. + manga_status.setText(when (manga.status) { + SManga.ONGOING -> R.string.ongoing + SManga.COMPLETED -> R.string.completed + SManga.LICENSED -> R.string.licensed + else -> R.string.unknown + }) + + // Update description TextView. + manga_summary.text = manga.description + + // Set the favorite drawable to the correct one. + setFavoriteDrawable(manga.favorite) + + // Set cover if it wasn't already. + if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { + Glide.with(context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .into(manga_cover) + + Glide.with(context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.RESULT) + .centerCrop() + .into(backdrop) + } + } + } + + /** + * Update chapter count TextView. + * + * @param count number of chapters. + */ + fun setChapterCount(count: Int) { + view?.manga_chapters?.text = count.toString() + } + + /** + * Toggles the favorite status and asks for confirmation to delete downloaded chapters. + */ + fun toggleFavorite() { + val view = view + + val isNowFavorite = presenter.toggleFavorite() + if (view != null && !isNowFavorite && presenter.hasDownloads()) { + view.snack(view.context.getString(R.string.delete_downloads_for_manga)) { + setAction(R.string.action_delete) { + presenter.deleteDownloads() + } + } + } + } + + /** + * Open the manga in browser. + */ + fun openInBrowser() { + val context = view?.context ?: return + val source = presenter.source as? HttpSource ?: return + + try { + val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString()) + val intent = CustomTabsIntent.Builder() + .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) + .build() + intent.launchUrl(activity, url) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. + */ + private fun shareManga() { + val context = view?.context ?: return + + val source = presenter.source as? HttpSource ?: return + try { + val url = source.mangaDetailsRequest(presenter.manga).url().toString() + val title = presenter.manga.title + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_text, title, url)) + } + startActivity(Intent.createChooser(intent, context.getString(R.string.action_share))) + } catch (e: Exception) { + context.toast(e.message) + } + } + + /** + * Update FAB with correct drawable. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteDrawable(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + view?.fab_favorite?.setImageResource(if (isFavorite) + R.drawable.ic_bookmark_white_24dp + else + R.drawable.ic_bookmark_border_white_24dp) + } + + /** + * Start fetching manga information from source. + */ + private fun fetchMangaFromSource() { + setRefreshing(true) + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource() + } + + + /** + * Update swipe refresh to stop showing refresh in progress spinner. + */ + fun onFetchMangaDone() { + setRefreshing(false) + } + + /** + * Update swipe refresh to start showing refresh in progress spinner. + */ + fun onFetchMangaError() { + setRefreshing(false) + } + + /** + * Set swipe refresh status. + * + * @param value whether it should be refreshing or not. + */ + private fun setRefreshing(value: Boolean) { + view?.swipe_refresh?.isRefreshing = value + } + + /** + * Called when the fab is clicked. + */ + private fun onFabClick() { + val manga = presenter.manga + toggleFavorite() + if (manga.favorite) { + val categories = presenter.getCategories() + val defaultCategory = categories.find { it.id == preferences.defaultCategory() } + if (defaultCategory != null) { + presenter.moveMangaToCategory(manga, defaultCategory) + } else if (categories.size <= 1) { // default or the one from the user + presenter.moveMangaToCategory(manga, categories.firstOrNull()) + } else { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.moveMangaToCategories(manga, categories) + } + + /** + * Add the manga to the home screen + */ + fun addToHomeScreen() { + val activity = activity ?: return + val mangaControllerArgs = parentController?.args ?: return + + val shortcutIntent = activity.intent + .setAction(MainActivity.SHORTCUT_MANGA) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(MangaController.MANGA_EXTRA, + mangaControllerArgs.getLong(MangaController.MANGA_EXTRA)) + + val addIntent = Intent("com.android.launcher.action.INSTALL_SHORTCUT") + .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) + + //Set shortcut title + val dialog = MaterialDialog.Builder(activity) + .title(R.string.shortcut_title) + .input("", presenter.manga.title, { _, text -> + //Set shortcut title + addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString()) + + reshapeIconBitmap(addIntent, + Glide.with(activity).load(presenter.manga).asBitmap()) + }) + .negativeText(android.R.string.cancel) + .show() + + untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() }) + } + + fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest) { + val activity = activity ?: return + + val modes = intArrayOf(R.string.circular_icon, + R.string.rounded_icon, + R.string.square_icon, + R.string.star_icon) + + fun BitmapRequestBuilder.toIcon(): Bitmap { + return this.into(96, 96).get() + } + + // i = 0: Circular icon + // i = 1: Rounded icon + // i = 2: Square icon + // i = 3: Star icon (because boredom) + fun getIcon(i: Int): Bitmap? { + return when (i) { + 0 -> request.transform(CropCircleTransformation(activity)).toIcon() + 1 -> request.transform(RoundedCornersTransformation(activity, 5, 0)).toIcon() + 2 -> request.transform(CropSquareTransformation(activity)).toIcon() + 3 -> request.transform(CenterCrop(activity), + MaskTransformation(activity, R.drawable.mask_star)).toIcon() + else -> null + } + } + + val dialog = MaterialDialog.Builder(activity) + .title(R.string.icon_shape) + .negativeText(android.R.string.cancel) + .items(modes.map { activity.getString(it) }) + .itemsCallback { _, _, i, _ -> + Observable.fromCallable { getIcon(i) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ icon -> + if (icon != null) createShortcut(addIntent, icon) + }, { + activity.toast(R.string.icon_creation_fail) + }) + } + .show() + + untilDestroySubscriptions.add(Subscriptions.create { dialog.dismiss() }) + } + + fun createShortcut(addIntent: Intent, icon: Bitmap) { + val activity = activity ?: return + + //Send shortcut intent + addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon) + activity.sendBroadcast(addIntent) + //Go to launcher to show this shiny new shortcut! + val startMain = Intent(Intent.ACTION_MAIN) + startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(startMain) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt deleted file mode 100644 index 5492e2456f..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt +++ /dev/null @@ -1,393 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import android.support.customtabs.CustomTabsIntent -import android.view.* -import android.widget.Toast -import com.afollestad.materialdialogs.MaterialDialog -import com.bumptech.glide.BitmapRequestBuilder -import com.bumptech.glide.BitmapTypeRequest -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.bitmap.CenterCrop -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.getResourceColor -import eu.kanade.tachiyomi.util.snack -import eu.kanade.tachiyomi.util.toast -import jp.wasabeef.glide.transformations.CropCircleTransformation -import jp.wasabeef.glide.transformations.CropSquareTransformation -import jp.wasabeef.glide.transformations.MaskTransformation -import jp.wasabeef.glide.transformations.RoundedCornersTransformation -import kotlinx.android.synthetic.main.fragment_manga_info.* -import nucleus.factory.RequiresPresenter -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -/** - * Fragment that shows manga information. - * Uses R.layout.fragment_manga_info. - * UI related actions should be called from here. - */ -@RequiresPresenter(MangaInfoPresenter::class) -class MangaInfoFragment : BaseRxFragment() { - - companion object { - /** - * Create new instance of MangaInfoFragment. - * - * @return MangaInfoFragment. - */ - fun newInstance(): MangaInfoFragment { - return MangaInfoFragment() - } - - } - - /** - * Preferences helper. - */ - private val preferences: PreferencesHelper by injectLazy() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_manga_info, container, false) - } - - override fun onViewCreated(view: View?, savedState: Bundle?) { - // Set onclickListener to toggle favorite when FAB clicked. - fab_favorite.setOnClickListener { - if(!presenter.manga.favorite) { - val defaultCategory = presenter.getCategories().find { it.id == preferences.defaultCategory()} - if(defaultCategory == null) { - onFabClick() - } else { - toggleFavorite() - presenter.moveMangaToCategory(defaultCategory, presenter.manga) - } - } else { - toggleFavorite() - } - } - - // Set SwipeRefresh to refresh manga data. - swipe_refresh.setOnRefreshListener { fetchMangaFromSource() } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.manga_info, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_open_in_browser -> openInBrowser() - R.id.action_share -> shareManga() - R.id.action_add_to_home_screen -> addToHomeScreen() - else -> return super.onOptionsItemSelected(item) - } - return true - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - fun onNextManga(manga: Manga, source: Source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source) - } else { - // Initialize manga. - fetchMangaFromSource() - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - private fun setMangaInfo(manga: Manga, source: Source?) { - // Update artist TextView. - manga_artist.text = manga.artist - - // Update author TextView. - manga_author.text = manga.author - - // If manga source is known update source TextView. - if (source != null) { - manga_source.text = source.toString() - } - - // Update genres TextView. - manga_genres.text = manga.genre - - // Update status TextView. - manga_status.setText(when (manga.status) { - SManga.ONGOING -> R.string.ongoing - SManga.COMPLETED -> R.string.completed - SManga.LICENSED -> R.string.licensed - else -> R.string.unknown - }) - - // Update description TextView. - manga_summary.text = manga.description - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite) - - // Set cover if it wasn't already. - if (manga_cover.drawable == null && !manga.thumbnail_url.isNullOrEmpty()) { - Glide.with(this) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(manga_cover) - - Glide.with(this) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESULT) - .centerCrop() - .into(backdrop) - } - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - fun setChapterCount(count: Int) { - manga_chapters.text = count.toString() - } - - /** - * Toggles the favorite status and asks for confirmation to delete downloaded chapters. - */ - fun toggleFavorite() { - if (!isAdded) return - - val isNowFavorite = presenter.toggleFavorite() - if (!isNowFavorite && presenter.hasDownloads()) { - view!!.snack(getString(R.string.delete_downloads_for_manga)) { - setAction(R.string.action_delete) { - presenter.deleteDownloads() - } - } - } - } - - /** - * Open the manga in browser. - */ - fun openInBrowser() { - if (!isAdded) return - - val source = presenter.source as? HttpSource ?: return - try { - val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString()) - val intent = CustomTabsIntent.Builder() - .setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) - .build() - intent.launchUrl(activity, url) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Called to run Intent with [Intent.ACTION_SEND], which show share dialog. - */ - private fun shareManga() { - if (!isAdded) return - - val source = presenter.source as? HttpSource ?: return - try { - val url = source.mangaDetailsRequest(presenter.manga).url().toString() - val sharingIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text, presenter.manga.title, url)) - } - startActivity(Intent.createChooser(sharingIntent, getString(R.string.action_share))) - } catch (e: Exception) { - context.toast(e.message) - } - } - - /** - * Add the manga to the home screen - */ - fun addToHomeScreen() { - if (!isAdded) return - - val shortcutIntent = activity.intent - shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(MangaActivity.FROM_LAUNCHER_EXTRA, true) - - val addIntent = Intent() - addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) - .action = "com.android.launcher.action.INSTALL_SHORTCUT" - - //Set shortcut title - MaterialDialog.Builder(activity) - .title(R.string.shortcut_title) - .input("", presenter.manga.title, { md, text -> - //Set shortcut title - addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, text.toString()) - - reshapeIconBitmap(addIntent, - Glide.with(context).load(presenter.manga).asBitmap()) - }) - .negativeText(android.R.string.cancel) - .onNegative { materialDialog, dialogAction -> materialDialog.cancel() } - .show() - } - - fun reshapeIconBitmap(addIntent: Intent, request: BitmapTypeRequest) { - val modes = intArrayOf(R.string.circular_icon, - R.string.rounded_icon, - R.string.square_icon, - R.string.star_icon) - - fun BitmapRequestBuilder.toIcon(): Bitmap { - return this.into(96, 96).get() - } - - MaterialDialog.Builder(activity) - .title(R.string.icon_shape) - .negativeText(android.R.string.cancel) - .items(modes.map { getString(it) }) - .itemsCallback { dialog, view, i, charSequence -> - Observable.fromCallable { - // i = 0: Circular icon - // i = 1: Rounded icon - // i = 2: Square icon - // i = 3: Star icon (because boredom) - when (i) { - 0 -> request.transform(CropCircleTransformation(context)).toIcon() - 1 -> request.transform(RoundedCornersTransformation(context, 5, 0)).toIcon() - 2 -> request.transform(CropSquareTransformation(context)).toIcon() - 3 -> request.transform(CenterCrop(context), MaskTransformation(context, R.drawable.mask_star)).toIcon() - else -> null - } - }.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ if (it != null) createShortcut(addIntent, it) }, - { context.toast(R.string.icon_creation_fail) }) - }.show() - } - - fun createShortcut(addIntent: Intent, icon: Bitmap) { - //Send shortcut intent - addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon) - context.sendBroadcast(addIntent) - //Go to launcher to show this shiny new shortcut! - val startMain = Intent(Intent.ACTION_MAIN) - startMain.addCategory(Intent.CATEGORY_HOME).flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(startMain) - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private fun setFavoriteDrawable(isFavorite: Boolean) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fab_favorite.setImageResource(if (isFavorite) - R.drawable.ic_bookmark_white_24dp - else - R.drawable.ic_bookmark_border_white_24dp) - } - - /** - * Start fetching manga information from source. - */ - private fun fetchMangaFromSource() { - setRefreshing(true) - // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() - } - - - /** - * Update swipe refresh to stop showing refresh in progress spinner. - */ - fun onFetchMangaDone() { - setRefreshing(false) - } - - /** - * Update swipe refresh to start showing refresh in progress spinner. - */ - fun onFetchMangaError() { - setRefreshing(false) - } - - /** - * Set swipe refresh status. - * - * @param value whether it should be refreshing or not. - */ - private fun setRefreshing(value: Boolean) { - swipe_refresh.isRefreshing = value - } - - /** - * Called when the fab is clicked. - */ - private fun onFabClick() { - val categories = presenter.getCategories() - - MaterialDialog.Builder(activity) - .title(R.string.action_move_category) - .items(categories.map { it.name }) - .itemsCallbackMultiChoice(presenter.getMangaCategoryIds(presenter.manga)) { dialog, position, text -> - if (position.contains(0) && position.count() > 1) { - dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray()) - Toast.makeText(dialog.context, R.string.invalid_combination, Toast.LENGTH_SHORT).show() - } - - true - } - .alwaysCallMultiChoiceCallback() - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog, _ -> - val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList() - - if(!selectedCategories.isEmpty()) { - if(!presenter.manga.favorite) { - toggleFavorite() - } - presenter.moveMangaToCategories(selectedCategories.filter { it.id != 0}, presenter.manga) - } else { - toggleFavorite() - } - } - .build() - .show() - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index f0f8963463..337dc1cfe6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -1,201 +1,169 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.os.Bundle -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.isNullOrUnsubscribed -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -class MangaInfoPresenter : BasePresenter() { - - /** - * Active manga. - */ - lateinit var manga: Manga - private set - - /** - * Source of the manga. - */ - lateinit var source: Source - private set - - /** - * Used to connect to database. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Used to connect to different manga sources. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Used to connect to cache. - */ - val coverCache: CoverCache by injectLazy() - - private val downloadManager: DownloadManager by injectLazy() - - /** - * Subscription to send the manga to the view. - */ - private var viewMangaSubcription: Subscription? = null - - /** - * Subscription to update the manga from the source. - */ - private var fetchMangaSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - source = sourceManager.get(manga.source)!! - sendMangaToView() - - // Update chapter count - SharedData.get(ChapterCountEvent::class.java)?.observable - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribeLatestCache(MangaInfoFragment::setChapterCount) - - // Update favorite status - SharedData.get(MangaFavoriteEvent::class.java)?.let { - it.observable - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { setFavorite(it) } - .apply { add(this) } - } - } - - /** - * Sends the active manga to the view. - */ - fun sendMangaToView() { - viewMangaSubcription?.let { remove(it) } - viewMangaSubcription = Observable.just(manga) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) - } - - /** - * Fetch manga information from source. - */ - fun fetchMangaFromSource() { - if (!fetchMangaSubscription.isNullOrUnsubscribed()) return - fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } - .map { networkManga -> - manga.copyFrom(networkManga) - manga.initialized = true - db.insertManga(manga).executeAsBlocking() - manga - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { sendMangaToView() } - .subscribeFirst({ view, manga -> - view.onFetchMangaDone() - }, { view, error -> - view.onFetchMangaError() - }) - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - * - * @return the new status of the manga. - */ - fun toggleFavorite(): Boolean { - manga.favorite = !manga.favorite - if (!manga.favorite) { - coverCache.deleteFromCache(manga.thumbnail_url) - } - db.insertManga(manga).executeAsBlocking() - sendMangaToView() - return manga.favorite - } - - private fun setFavorite(favorite: Boolean) { - if (manga.favorite == favorite) { - return - } - toggleFavorite() - } - - /** - * Returns true if the manga has any downloads. - */ - fun hasDownloads(): Boolean { - return downloadManager.findMangaDir(source, manga) != null - } - - /** - * Deletes all the downloads for the manga. - */ - fun deleteDownloads() { - downloadManager.findMangaDir(source, manga)?.delete() - } - - /** - * Get the default, and user categories. - * - * @return List of categories, default plus user categories - */ - fun getCategories(): List { - return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking() - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - if(categories.isEmpty()) { - return arrayListOf(Category.createDefault().id).toTypedArray() - } - return categories.map { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param categories the selected categories. - * @param manga the manga to move. - */ - fun moveMangaToCategories(categories: List, manga: Manga) { - val mc = categories.map { MangaCategory.create(manga, it) } - - db.setMangaCategories(mc, arrayListOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param category the selected category. - * @param manga the manga to move. - */ - fun moveMangaToCategory(category: Category, manga: Manga) { - moveMangaToCategories(arrayListOf(category), manga) - } - -} +package eu.kanade.tachiyomi.ui.manga.info + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.isNullOrUnsubscribed +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of MangaInfoFragment. + * Contains information and data for fragment. + * Observable updates should be called from here. + */ +class MangaInfoPresenter( + val manga: Manga, + val source: Source, + private val chapterCountRelay: BehaviorRelay, + private val mangaFavoriteRelay: PublishRelay, + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { + + /** + * Subscription to send the manga to the view. + */ + private var viewMangaSubcription: Subscription? = null + + /** + * Subscription to update the manga from the source. + */ + private var fetchMangaSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + sendMangaToView() + + // Update chapter count + chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(MangaInfoController::setChapterCount) + + // Update favorite status + mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread()) + .subscribe { setFavorite(it) } + .apply { add(this) } + } + + /** + * Sends the active manga to the view. + */ + fun sendMangaToView() { + viewMangaSubcription?.let { remove(it) } + viewMangaSubcription = Observable.just(manga) + .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) + } + + /** + * Fetch manga information from source. + */ + fun fetchMangaFromSource() { + if (!fetchMangaSubscription.isNullOrUnsubscribed()) return + fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } + .map { networkManga -> + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + manga + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { sendMangaToView() } + .subscribeFirst({ view, _ -> + view.onFetchMangaDone() + }, { view, _ -> + view.onFetchMangaError() + }) + } + + /** + * Update favorite status of manga, (removes / adds) manga (to / from) library. + * + * @return the new status of the manga. + */ + fun toggleFavorite(): Boolean { + manga.favorite = !manga.favorite + if (!manga.favorite) { + coverCache.deleteFromCache(manga.thumbnail_url) + } + db.insertManga(manga).executeAsBlocking() + sendMangaToView() + return manga.favorite + } + + private fun setFavorite(favorite: Boolean) { + if (manga.favorite == favorite) { + return + } + toggleFavorite() + } + + /** + * Returns true if the manga has any downloads. + */ + fun hasDownloads(): Boolean { + return downloadManager.findMangaDir(source, manga) != null + } + + /** + * Deletes all the downloads for the manga. + */ + fun deleteDownloads() { + downloadManager.findMangaDir(source, manga)?.delete() + } + + /** + * Get the default, and user categories. + * + * @return List of categories, default plus user categories + */ + fun getCategories(): List { + return db.getCategories().executeAsBlocking() + } + + /** + * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. + * + * @param manga the manga to get categories from. + * @return Array of category ids the manga is in, if none returns default id + */ + fun getMangaCategoryIds(manga: Manga): Array { + val categories = db.getCategoriesForManga(manga).executeAsBlocking() + return categories.mapNotNull { it.id }.toTypedArray() + } + + /** + * Move the given manga to categories. + * + * @param manga the manga to move. + * @param categories the selected categories. + */ + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + } + + /** + * Move the given manga to the category. + * + * @param manga the manga to move. + * @param category the selected category, or null for default category. + */ + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt new file mode 100644 index 0000000000..61cda0174b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackChaptersDialog : DialogController + where T : Controller, T : SetTrackChaptersDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.chapters) + .customView(R.layout.dialog_track_chapters, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + np.clearFocus() + + (targetController as? Listener)?.setChaptersRead(item, np.value) + } + } + .build() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + // Set initial value + np.value = item.track?.last_chapter_read ?: 0 + // Don't allow to go from 0 to 9999 + np.wrapSelectorWheel = false + } + + return dialog + } + + interface Listener { + fun setChaptersRead(item: TrackItem, chaptersRead: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt new file mode 100644 index 0000000000..987a8b39a8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt @@ -0,0 +1,80 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackScoreDialog : DialogController + where T : Controller, T : SetTrackScoreDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.score) + .customView(R.layout.dialog_track_score, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog, _ -> + val view = dialog.customView + if (view != null) { + // Remove focus to update selected number + val np = view.findViewById(R.id.score_picker) as NumberPicker + np.clearFocus() + + (targetController as? Listener)?.setScore(item, np.value) + } + } + .show() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.score_picker) as NumberPicker + val scores = item.service.getScoreList().toTypedArray() + np.maxValue = scores.size - 1 + np.displayedValues = scores + + // Set initial value + val displayedScore = item.service.displayScore(item.track!!) + if (displayedScore != "-") { + val index = scores.indexOf(displayedScore) + np.value = if (index != -1) index else 0 + } + } + + return dialog + } + + interface Listener { + fun setScore(item: TrackItem, score: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt new file mode 100644 index 0000000000..6ad0579519 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SetTrackStatusDialog : DialogController + where T : Controller, T : SetTrackStatusDialog.Listener { + + private val item: TrackItem + + constructor(target: T, item: TrackItem) : super(Bundle().apply { + putSerializable(KEY_ITEM_TRACK, item.track) + }) { + targetController = target + this.item = item + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track + val service = Injekt.get().getService(track.sync_id)!! + item = TrackItem(track, service) + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val item = item + val statusList = item.service.getStatusList().orEmpty() + val statusString = statusList.mapNotNull { item.service.getStatus(it) } + val selectedIndex = statusList.indexOf(item.track?.status) + + return MaterialDialog.Builder(activity!!) + .title(R.string.status) + .negativeText(android.R.string.cancel) + .items(statusString) + .itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ -> + (targetController as? Listener)?.setStatus(item, i) + true + }) + .build() + } + + interface Listener { + fun setStatus(item: TrackItem, selection: Int) + } + + private companion object { + const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 08e727b978..09b06bcba0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -1,33 +1,44 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.support.v7.widget.RecyclerView -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.inflate - -class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter() { - - var items = emptyList() - set(value) { - if (field !== value) { - field = value - notifyDataSetChanged() - } - } - - var onClickListener: (TrackItem) -> Unit = {} - - override fun getItemCount(): Int { - return items.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { - val view = parent.inflate(R.layout.item_track) - return TrackHolder(view, fragment) - } - - override fun onBindViewHolder(holder: TrackHolder, position: Int) { - holder.onSetValues(items[position]) - } - -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.inflate + +class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { + + var items = emptyList() + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + val rowClickListener: OnRowClickListener = controller + + fun getItem(index: Int): TrackItem? { + return items.getOrNull(index) + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { + val view = parent.inflate(R.layout.item_track) + return TrackHolder(view, this) + } + + override fun onBindViewHolder(holder: TrackHolder, position: Int) { + holder.bind(items[position]) + } + + interface OnRowClickListener { + fun onTitleClick(position: Int) + fun onStatusClick(position: Int) + fun onChaptersClick(position: Int) + fun onScoreClick(position: Int) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt new file mode 100644 index 0000000000..c38c2bdc0e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt @@ -0,0 +1,123 @@ +package eu.kanade.tachiyomi.ui.manga.track + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.fragment_track.view.* + +class TrackController : NucleusController(), + TrackAdapter.OnRowClickListener, + SetTrackStatusDialog.Listener, + SetTrackChaptersDialog.Listener, + SetTrackScoreDialog.Listener { + + private var adapter: TrackAdapter? = null + + override fun createPresenter(): TrackPresenter { + return TrackPresenter((parentController as MangaController).manga!!) + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.fragment_track, container, false) + } + + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = TrackAdapter(this) + with(view) { + track_recycler.layoutManager = LinearLayoutManager(context) + track_recycler.adapter = adapter + swipe_refresh.isEnabled = false + swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + fun onNextTrackings(trackings: List) { + val atLeastOneLink = trackings.any { it.track != null } + adapter?.items = trackings + view?.swipe_refresh?.isEnabled = atLeastOneLink + (parentController as? MangaController)?.setTrackingIcon(atLeastOneLink) + } + + fun onSearchResults(results: List) { + getSearchDialog()?.onSearchResults(results) + } + + @Suppress("UNUSED_PARAMETER") + fun onSearchResultsError(error: Throwable) { + getSearchDialog()?.onSearchResultsError() + } + + private fun getSearchDialog(): TrackSearchDialog? { + return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog + } + + fun onRefreshDone() { + view?.swipe_refresh?.isRefreshing = false + } + + fun onRefreshError(error: Throwable) { + view?.swipe_refresh?.isRefreshing = false + activity?.toast(error.message) + } + + override fun onTitleClick(position: Int) { + val item = adapter?.getItem(position) ?: return + TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER) + } + + override fun onStatusClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackStatusDialog(this, item).showDialog(router) + } + + override fun onChaptersClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackChaptersDialog(this, item).showDialog(router) + } + + override fun onScoreClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackScoreDialog(this, item).showDialog(router) + } + + override fun setStatus(item: TrackItem, selection: Int) { + presenter.setStatus(item, selection) + view?.swipe_refresh?.isRefreshing = true + } + + override fun setScore(item: TrackItem, score: Int) { + presenter.setScore(item, score) + view?.swipe_refresh?.isRefreshing = true + } + + override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { + presenter.setLastChapterRead(item, chaptersRead) + view?.swipe_refresh?.isRefreshing = true + } + + private companion object { + const val TAG_SEARCH_CONTROLLER = "track_search_controller" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt deleted file mode 100644 index 017ef8703a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackFragment.kt +++ /dev/null @@ -1,173 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import android.support.v7.widget.LinearLayoutManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.NumberPicker -import com.afollestad.materialdialogs.MaterialDialog -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.fragment_track.* -import nucleus.factory.RequiresPresenter - -@RequiresPresenter(TrackPresenter::class) -class TrackFragment : BaseRxFragment() { - - companion object { - fun newInstance(): TrackFragment { - return TrackFragment() - } - } - - private lateinit var adapter: TrackAdapter - - private var dialog: TrackSearchDialog? = null - - private val searchFragmentTag: String - get() = "search_fragment" - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { - return inflater.inflate(R.layout.fragment_track, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - adapter = TrackAdapter(this) - recycler.layoutManager = LinearLayoutManager(context) - recycler.adapter = adapter - swipe_refresh.isEnabled = false - swipe_refresh.setOnRefreshListener { presenter.refresh() } - } - - private fun findSearchFragmentIfNeeded() { - if (dialog == null) { - dialog = childFragmentManager.findFragmentByTag(searchFragmentTag) as? TrackSearchDialog - } - } - - fun onNextTrackings(trackings: List) { - adapter.items = trackings - swipe_refresh.isEnabled = trackings.any { it.track != null } - (activity as MangaActivity).setTrackingIcon(trackings.any { it.track != null }) - } - - fun onSearchResults(results: List) { - if (!isResumed) return - - findSearchFragmentIfNeeded() - dialog?.onSearchResults(results) - } - - fun onSearchResultsError(error: Throwable) { - if (!isResumed) return - - findSearchFragmentIfNeeded() - dialog?.onSearchResultsError() - } - - fun onRefreshDone() { - swipe_refresh.isRefreshing = false - } - - fun onRefreshError(error: Throwable) { - swipe_refresh.isRefreshing = false - context.toast(error.message) - } - - fun onTitleClick(item: TrackItem) { - if (!isResumed) return - - if (dialog == null) { - dialog = TrackSearchDialog.newInstance() - } - - presenter.selectedService = item.service - dialog?.show(childFragmentManager, searchFragmentTag) - } - - fun onStatusClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val statusList = item.service.getStatusList().map { item.service.getStatus(it) } - val selectedIndex = item.service.getStatusList().indexOf(item.track.status) - - MaterialDialog.Builder(context) - .title(R.string.status) - .items(statusList) - .itemsCallbackSingleChoice(selectedIndex, { dialog, view, i, charSequence -> - presenter.setStatus(item, i) - swipe_refresh.isRefreshing = true - true - }) - .show() - } - - fun onChaptersClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val dialog = MaterialDialog.Builder(context) - .title(R.string.chapters) - .customView(R.layout.dialog_track_chapters, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { d, action -> - val view = d.customView - if (view != null) { - val np = view.findViewById(R.id.chapters_picker) as NumberPicker - np.clearFocus() - presenter.setLastChapterRead(item, np.value) - swipe_refresh.isRefreshing = true - } - } - .show() - - val view = dialog.customView - if (view != null) { - val np = view.findViewById(R.id.chapters_picker) as NumberPicker - // Set initial value - np.value = item.track.last_chapter_read - // Don't allow to go from 0 to 9999 - np.wrapSelectorWheel = false - } - } - - fun onScoreClick(item: TrackItem) { - if (!isResumed || item.track == null) return - - val dialog = MaterialDialog.Builder(activity) - .title(R.string.score) - .customView(R.layout.dialog_track_score, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { d, action -> - val view = d.customView - if (view != null) { - val np = view.findViewById(R.id.score_picker) as NumberPicker - np.clearFocus() - presenter.setScore(item, np.value) - swipe_refresh.isRefreshing = true - } - } - .show() - - val view = dialog.customView - if (view != null) { - val np = view.findViewById(R.id.score_picker) as NumberPicker - val scores = item.service.getScoreList().toTypedArray() - np.maxValue = scores.size - 1 - np.displayedValues = scores - - // Set initial value - val displayedScore = item.service.displayScore(item.track) - if (displayedScore != "-") { - val index = scores.indexOf(displayedScore) - np.value = if (index != -1) index else 0 - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 9ca33f6925..98176bfb53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -1,42 +1,41 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.support.v7.widget.RecyclerView -import android.view.View -import eu.kanade.tachiyomi.R -import kotlinx.android.synthetic.main.item_track.view.* - -class TrackHolder(private val view: View, private val fragment: TrackFragment) -: RecyclerView.ViewHolder(view) { - - private lateinit var item: TrackItem - - init { - view.title_container.setOnClickListener { fragment.onTitleClick(item) } - view.status_container.setOnClickListener { fragment.onStatusClick(item) } - view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) } - view.score_container.setOnClickListener { fragment.onScoreClick(item) } - } - - @Suppress("DEPRECATION") - fun onSetValues(item: TrackItem) = with(view) { - this@TrackHolder.item = item - val track = item.track - track_logo.setImageResource(item.service.getLogo()) - logo.setBackgroundColor(item.service.getLogoColor()) - if (track != null) { - track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) - track_title.setAllCaps(false) - track_title.text = track.title - track_chapters.text = "${track.last_chapter_read}/" + - if (track.total_chapters > 0) track.total_chapters else "-" - track_status.text = item.service.getStatus(track.status) - track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) - } else { - track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) - track_title.setText(R.string.action_edit) - track_chapters.text = "" - track_score.text = "" - track_status.text = "" - } - } -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import android.annotation.SuppressLint +import android.support.v7.widget.RecyclerView +import android.view.View +import eu.kanade.tachiyomi.R +import kotlinx.android.synthetic.main.item_track.view.* + +class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) { + + init { + val listener = adapter.rowClickListener + view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } + view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } + view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } + view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } + } + + @SuppressLint("SetTextI18n") + @Suppress("DEPRECATION") + fun bind(item: TrackItem) = with(itemView) { + val track = item.track + track_logo.setImageResource(item.service.getLogo()) + logo.setBackgroundColor(item.service.getLogoColor()) + if (track != null) { + track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary) + track_title.setAllCaps(false) + track_title.text = track.title + track_chapters.text = "${track.last_chapter_read}/" + + if (track.total_chapters > 0) track.total_chapters else "-" + track_status.text = item.service.getStatus(track.status) + track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) + } else { + track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button) + track_title.setText(R.string.action_edit) + track_chapters.text = "" + track_score.text = "" + track_status.text = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt index 9a435cd326..6e7c3ebeca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackItem.kt @@ -1,8 +1,6 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackService - -class TrackItem(val track: Track?, val service: TrackService) { - -} \ No newline at end of file +package eu.kanade.tachiyomi.ui.manga.track + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService + +data class TrackItem(val track: Track?, val service: TrackService) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt index 6d799b0c46..b9e152ba44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -1,137 +1,129 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.os.Bundle -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.manga.MangaEvent -import eu.kanade.tachiyomi.util.SharedData -import eu.kanade.tachiyomi.util.toast -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import uy.kohesive.injekt.injectLazy - -class TrackPresenter : BasePresenter() { - - private val db: DatabaseHelper by injectLazy() - - private val trackManager: TrackManager by injectLazy() - - lateinit var manga: Manga - private set - - private var trackList: List = emptyList() - - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - - var selectedService: TrackService? = null - - private var trackSubscription: Subscription? = null - - private var searchSubscription: Subscription? = null - - private var refreshSubscription: Subscription? = null - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - - manga = SharedData.get(MangaEvent::class.java)?.manga ?: return - fetchTrackings() - } - - fun fetchTrackings() { - trackSubscription?.let { remove(it) } - trackSubscription = db.getTracks(manga) - .asRxObservable() - .map { tracks -> - loggedServices.map { service -> - TrackItem(tracks.find { it.sync_id == service.id }, service) - } - } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { trackList = it } - .subscribeLatestCache(TrackFragment::onNextTrackings) - } - - fun refresh() { - refreshSubscription?.let { remove(it) } - refreshSubscription = Observable.from(trackList) - .filter { it.track != null } - .concatMap { item -> - item.service.refresh(item.track!!) - .flatMap { db.insertTrack(it).asRxObservable() } - .map { item } - .onErrorReturn { item } - } - .toList() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, - TrackFragment::onRefreshError) - } - - fun search(query: String) { - val service = selectedService ?: return - - searchSubscription?.let { remove(it) } - searchSubscription = service.search(query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(TrackFragment::onSearchResults, - TrackFragment::onSearchResultsError) - } - - fun registerTracking(item: Track?) { - val service = selectedService ?: return - - if (item != null) { - item.manga_id = manga.id!! - add(service.bind(item) - .flatMap { db.insertTrack(item).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ }, - { error -> context.toast(error.message) })) - } else { - db.deleteTrackForManga(manga, service).executeAsBlocking() - } - } - - private fun updateRemote(track: Track, service: TrackService) { - service.update(track) - .flatMap { db.insertTrack(track).asRxObservable() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> view.onRefreshDone() }, - { view, error -> - view.onRefreshError(error) - - // Restart on error to set old values - fetchTrackings() - }) - } - - fun setStatus(item: TrackItem, index: Int) { - val track = item.track!! - track.status = item.service.getStatusList()[index] - updateRemote(track, item.service) - } - - fun setScore(item: TrackItem, index: Int) { - val track = item.track!! - track.score = item.service.indexToScore(index) - updateRemote(track, item.service) - } - - fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { - val track = item.track!! - track.last_chapter_read = chapterNumber - updateRemote(track, item.service) - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.toast +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class TrackPresenter( + val manga: Manga, + preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val trackManager: TrackManager = Injekt.get() +) : BasePresenter() { + + private val context = preferences.context + + private var trackList: List = emptyList() + + private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } + + private var trackSubscription: Subscription? = null + + private var searchSubscription: Subscription? = null + + private var refreshSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + fetchTrackings() + } + + fun fetchTrackings() { + trackSubscription?.let { remove(it) } + trackSubscription = db.getTracks(manga) + .asRxObservable() + .map { tracks -> + loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { trackList = it } + .subscribeLatestCache(TrackController::onNextTrackings) + } + + fun refresh() { + refreshSubscription?.let { remove(it) } + refreshSubscription = Observable.from(trackList) + .filter { it.track != null } + .concatMap { item -> + item.service.refresh(item.track!!) + .flatMap { db.insertTrack(it).asRxObservable() } + .map { item } + .onErrorReturn { item } + } + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, result -> view.onRefreshDone() }, + TrackController::onRefreshError) + } + + fun search(query: String, service: TrackService) { + searchSubscription?.let { remove(it) } + searchSubscription = service.search(query) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(TrackController::onSearchResults, + TrackController::onSearchResultsError) + } + + fun registerTracking(item: Track?, service: TrackService) { + if (item != null) { + item.manga_id = manga.id!! + add(service.bind(item) + .flatMap { db.insertTrack(item).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ }, + { error -> context.toast(error.message) })) + } else { + db.deleteTrackForManga(manga, service).executeAsBlocking() + } + } + + private fun updateRemote(track: Track, service: TrackService) { + service.update(track) + .flatMap { db.insertTrack(track).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst({ view, result -> view.onRefreshDone() }, + { view, error -> + view.onRefreshError(error) + + // Restart on error to set old values + fetchTrackings() + }) + } + + fun setStatus(item: TrackItem, index: Int) { + val track = item.track!! + track.status = item.service.getStatusList()[index] + updateRemote(track, item.service) + } + + fun setScore(item: TrackItem, index: Int) { + val track = item.track!! + track.score = item.service.indexToScore(index) + updateRemote(track, item.service) + } + + fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { + val track = item.track!! + track.last_chapter_read = chapterNumber + updateRemote(track, item.service) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt index 7aa5f7653d..6a4ebc07c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt @@ -1,47 +1,47 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.util.inflate -import kotlinx.android.synthetic.main.item_track_search.view.* -import java.util.* - -class TrackSearchAdapter(context: Context) -: ArrayAdapter(context, R.layout.item_track_search, ArrayList()) { - - override fun getView(position: Int, view: View?, parent: ViewGroup): View { - var v = view - // Get the data item for this position - val track = getItem(position) - // Check if an existing view is being reused, otherwise inflate the view - val holder: TrackSearchHolder // view lookup cache stored in tag - if (v == null) { - v = parent.inflate(R.layout.item_track_search) - holder = TrackSearchHolder(v) - v.tag = holder - } else { - holder = v.tag as TrackSearchHolder - } - holder.onSetValues(track) - return v - } - - fun setItems(syncs: List) { - setNotifyOnChange(false) - clear() - addAll(syncs) - notifyDataSetChanged() - } - - class TrackSearchHolder(private val view: View) { - - fun onSetValues(track: Track) { - view.track_search_title.text = track.title - } - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.item_track_search.view.* +import java.util.* + +class TrackSearchAdapter(context: Context) +: ArrayAdapter(context, R.layout.item_track_search, ArrayList()) { + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + var v = view + // Get the data item for this position + val track = getItem(position) + // Check if an existing view is being reused, otherwise inflate the view + val holder: TrackSearchHolder // view lookup cache stored in tag + if (v == null) { + v = parent.inflate(R.layout.item_track_search) + holder = TrackSearchHolder(v) + v.tag = holder + } else { + holder = v.tag as TrackSearchHolder + } + holder.onSetValues(track) + return v + } + + fun setItems(syncs: List) { + setNotifyOnChange(false) + clear() + addAll(syncs) + notifyDataSetChanged() + } + + class TrackSearchHolder(private val view: View) { + + fun onSetValues(track: Track) { + view.track_search_title.text = track.title + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index 787180600f..8af0cc8470 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -1,119 +1,144 @@ -package eu.kanade.tachiyomi.ui.manga.track - -import android.app.Dialog -import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.widget.SimpleTextWatcher -import kotlinx.android.synthetic.main.dialog_track_search.view.* -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -class TrackSearchDialog : DialogFragment() { - - companion object { - - fun newInstance(): TrackSearchDialog { - return TrackSearchDialog() - } - } - - private lateinit var v: View - - lateinit var adapter: TrackSearchAdapter - private set - - private val queryRelay by lazy { PublishRelay.create() } - - private var searchDebounceSubscription: Subscription? = null - - private var selectedItem: Track? = null - - val presenter: TrackPresenter - get() = (parentFragment as TrackFragment).presenter - - override fun onCreateDialog(savedState: Bundle?): Dialog { - val dialog = MaterialDialog.Builder(context) - .customView(R.layout.dialog_track_search, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { dialog1, which -> onPositiveButtonClick() } - .build() - - onViewCreated(dialog.view, savedState) - - return dialog - } - - override fun onViewCreated(view: View, savedState: Bundle?) { - v = view - - // Create adapter - adapter = TrackSearchAdapter(context) - view.track_search_list.adapter = adapter - - // Set listeners - selectedItem = null - view.track_search_list.setOnItemClickListener { parent, viewList, position, id -> - selectedItem = adapter.getItem(position) - } - - // Do an initial search based on the manga's title - if (savedState == null) { - val title = presenter.manga.title - view.track_search.append(title) - search(title) - } - - view.track_search.addTextChangedListener(object : SimpleTextWatcher() { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - queryRelay.call(s.toString()) - } - }) - } - - override fun onResume() { - super.onResume() - - // Listen to text changes - searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .filter { it.isNotBlank() } - .subscribe { search(it) } - } - - override fun onPause() { - searchDebounceSubscription?.unsubscribe() - super.onPause() - } - - private fun search(query: String) { - v.progress.visibility = View.VISIBLE - v.track_search_list.visibility = View.GONE - - presenter.search(query) - } - - fun onSearchResults(results: List) { - selectedItem = null - v.progress.visibility = View.GONE - v.track_search_list.visibility = View.VISIBLE - adapter.setItems(results) - } - - fun onSearchResultsError() { - v.progress.visibility = View.VISIBLE - v.track_search_list.visibility = View.GONE - adapter.setItems(emptyList()) - } - - private fun onPositiveButtonClick() { - presenter.registerTracking(selectedItem) - } - +package eu.kanade.tachiyomi.ui.manga.track + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import com.jakewharton.rxbinding.widget.itemClicks +import com.jakewharton.rxbinding.widget.textChanges +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.plusAssign +import kotlinx.android.synthetic.main.dialog_track_search.view.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subscriptions.CompositeSubscription +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit + +class TrackSearchDialog : DialogController { + + private var dialogView: View? = null + + private var adapter: TrackSearchAdapter? = null + + private var selectedItem: Track? = null + + private val service: TrackService + + private var subscriptions = CompositeSubscription() + + private var searchTextSubscription: Subscription? = null + + private val trackController + get() = targetController as TrackController + + constructor(target: TrackController, service: TrackService) : super(Bundle().apply { + putInt(KEY_SERVICE, service.id) + }) { + targetController = target + this.service = service + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + service = Injekt.get().getService(bundle.getInt(KEY_SERVICE))!! + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity!!) + .customView(R.layout.dialog_track_search, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> onPositiveButtonClick() } + .build() + + if (subscriptions.isUnsubscribed) { + subscriptions = CompositeSubscription() + } + + dialogView = dialog.view + onViewCreated(dialog.view, savedState) + + return dialog + } + + fun onViewCreated(view: View, savedState: Bundle?) { + // Create adapter + val adapter = TrackSearchAdapter(view.context) + this.adapter = adapter + view.track_search_list.adapter = adapter + + // Set listeners + selectedItem = null + + subscriptions += view.track_search_list.itemClicks().subscribe { position -> + selectedItem = adapter.getItem(position) + } + + // Do an initial search based on the manga's title + if (savedState == null) { + val title = trackController.presenter.manga.title + view.track_search.append(title) + search(title) + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + subscriptions.unsubscribe() + dialogView = null + adapter = null + } + + override fun onAttach(view: View) { + super.onAttach(view) + searchTextSubscription = dialogView!!.track_search.textChanges() + .skip(1) + .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) + .map { it.toString() } + .filter(String::isNotBlank) + .subscribe { search(it) } + } + + override fun onDetach(view: View) { + super.onDetach(view) + searchTextSubscription?.unsubscribe() + } + + private fun search(query: String) { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.GONE + + trackController.presenter.search(query, service) + } + + fun onSearchResults(results: List) { + selectedItem = null + val view = dialogView ?: return + view.progress.visibility = View.GONE + view.track_search_list.visibility = View.VISIBLE + adapter?.setItems(results) + } + + fun onSearchResultsError() { + val view = dialogView ?: return + view.progress.visibility = View.VISIBLE + view.track_search_list.visibility = View.GONE + adapter?.setItems(emptyList()) + } + + private fun onPositiveButtonClick() { + trackController.presenter.registerTracking(selectedItem, service) + } + + private companion object { + const val KEY_SERVICE = "service_id" + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 96f0184eef..e5c52abcd6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -28,7 +28,8 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import timber.log.Timber -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.File import java.net.URLConnection import java.util.* @@ -36,41 +37,17 @@ import java.util.* /** * Presenter of [ReaderActivity]. */ -class ReaderPresenter : BasePresenter() { - /** - * Preferences. - */ - val prefs: PreferencesHelper by injectLazy() +class ReaderPresenter( + val prefs: PreferencesHelper = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), + val downloadManager: DownloadManager = Injekt.get(), + val trackManager: TrackManager = Injekt.get(), + val sourceManager: SourceManager = Injekt.get(), + val chapterCache: ChapterCache = Injekt.get(), + val coverCache: CoverCache = Injekt.get() +) : BasePresenter() { - /** - * Database. - */ - val db: DatabaseHelper by injectLazy() - - /** - * Download manager. - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Tracking manager. - */ - val trackManager: TrackManager by injectLazy() - - /** - * Source manager. - */ - val sourceManager: SourceManager by injectLazy() - - /** - * Chapter cache. - */ - val chapterCache: ChapterCache by injectLazy() - - /** - * Cover cache. - */ - val coverCache: CoverCache by injectLazy() + private val context = prefs.context /** * Manga being read. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt index 878ab38df1..ed1d126388 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base +import android.support.v4.app.Fragment import com.davemorrissey.labs.subscaleview.decoder.* import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderChapter import java.util.* @@ -12,7 +12,7 @@ import java.util.* * Base reader containing the common data that can be used by its implementations. It does not * contain any UI related action. */ -abstract class BaseReader : BaseFragment() { +abstract class BaseReader : Fragment() { companion object { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt new file mode 100644 index 0000000000..1e6c058dea --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/ConfirmDeleteChaptersDialog.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.ui.recent_updates + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class ConfirmDeleteChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : ConfirmDeleteChaptersDialog.Listener { + + private var chaptersToDelete = emptyList() + + constructor(target: T, chaptersToDelete: List) : this() { + this.chaptersToDelete = chaptersToDelete + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .content(R.string.confirm_delete_chapters) + .positiveText(android.R.string.yes) + .negativeText(android.R.string.no) + .onPositive { _, _ -> + (targetController as? Listener)?.deleteChapters(chaptersToDelete) + } + .build() + } + + interface Listener { + fun deleteChapters(chaptersToDelete: List) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt new file mode 100644 index 0000000000..8bb9d57b99 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/DeletingChaptersDialog.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.recent_updates + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Router +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) { + + companion object { + const val TAG = "deleting_dialog" + } + + override fun onCreateDialog(savedState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.deleting) + .build() + } + + override fun showDialog(router: Router) { + showDialog(router, TAG) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt index 7ee7e1fe9e..0855fde502 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt @@ -115,7 +115,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> - with(adapter.fragment) { + with(adapter.controller) { when (menuItem.itemId) { R.id.action_download -> downloadChapter(item) R.id.action_delete -> deleteChapter(item) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt index 7f1a1e4fd7..71042e7ce8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterItem.kt @@ -27,11 +27,19 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem return R.layout.item_recent_chapters } - override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): RecentChapterHolder { - return RecentChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as RecentChaptersAdapter) + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): RecentChapterHolder { + + val view = inflater.inflate(layoutRes, parent, false) + return RecentChapterHolder(view , adapter as RecentChaptersAdapter) } - override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: RecentChapterHolder, position: Int, payloads: List?) { + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: RecentChapterHolder, + position: Int, + payloads: List?) { + holder.bind(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt index ebd1b7e694..6be10bc38c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt @@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.recent_updates import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible -class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : - FlexibleAdapter>(null, fragment, true) { +class RecentChaptersAdapter(val controller: RecentChaptersController) : + FlexibleAdapter>(null, controller, true) { init { setDisplayHeadersAtStartUp(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt similarity index 59% rename from app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt index 338f28d51e..75e7034b4f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersController.kt @@ -1,340 +1,323 @@ -package eu.kanade.tachiyomi.ui.recent_updates - -import android.os.Bundle -import android.support.v4.app.DialogFragment -import android.support.v7.app.AppCompatActivity -import android.support.v7.view.ActionMode -import android.support.v7.widget.DividerItemDecoration -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView -import android.view.* -import com.afollestad.materialdialogs.MaterialDialog -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.toast -import eu.kanade.tachiyomi.widget.DeletingChaptersDialog -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.fragment_recent_chapters.* -import nucleus.factory.RequiresPresenter -import timber.log.Timber - -/** - * Fragment that shows recent chapters. - * Uses [R.layout.fragment_recent_chapters]. - * UI related actions should be called from here. - */ -@RequiresPresenter(RecentChaptersPresenter::class) -class RecentChaptersFragment: - BaseRxFragment(), - ActionMode.Callback, - FlexibleAdapter.OnItemClickListener, - FlexibleAdapter.OnItemLongClickListener{ - - companion object { - /** - * Create new RecentChaptersFragment. - * @return a new instance of [RecentChaptersFragment]. - */ - fun newInstance(): RecentChaptersFragment { - return RecentChaptersFragment() - } - } - - /** - * Action mode for multiple selection. - */ - private var actionMode: ActionMode? = null - - /** - * Adapter containing the recent chapters. - */ - lateinit var adapter: RecentChaptersAdapter - private set - - /** - * Called when view gets created - * @param inflater layout inflater - * @param container view group - * @param savedState status of saved state - */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { - // Inflate view - return inflater.inflate(R.layout.fragment_recent_chapters, container, false) - } - - /** - * Called when view is created - * @param view created view - * @param savedState status of saved sate - */ - override fun onViewCreated(view: View, savedState: Bundle?) { - // Init RecyclerView and adapter - recycler.layoutManager = LinearLayoutManager(activity) - recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) - recycler.setHasFixedSize(true) - adapter = RecentChaptersAdapter(this) - recycler.adapter = adapter - - recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) { - // Disable swipe refresh when view is not at the top - val firstPos = (recycler.layoutManager as LinearLayoutManager) - .findFirstCompletelyVisibleItemPosition() - swipe_refresh.isEnabled = firstPos == 0 - } - }) - - swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) - swipe_refresh.setOnRefreshListener { - if (!LibraryUpdateService.isRunning(activity)) { - LibraryUpdateService.start(activity) - context.toast(R.string.action_update_library) - } - // It can be a very long operation, so we disable swipe refresh and show a toast. - swipe_refresh.isRefreshing = false - } - - // Update toolbar text - setToolbarTitle(R.string.label_recent_updates) - - // Disable toolbar elevation, it looks better with sticky headers. - activity.appbar.disableElevation() - } - - override fun onDestroyView() { - // Restore toolbar elevation. - activity.appbar.enableElevation() - super.onDestroyView() - } - - /** - * Returns selected chapters - * @return list of selected chapters - */ - fun getSelectedChapters(): List { - return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } - } - - /** - * Called when item in list is clicked - * @param position position of clicked item - */ - override fun onItemClick(position: Int): Boolean { - // Get item from position - val item = adapter.getItem(position) as? RecentChapterItem ?: return false - if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { - toggleSelection(position) - return true - } else { - openChapter(item) - return false - } - } - - /** - * Called when item in list is long clicked - * @param position position of clicked item - */ - override fun onItemLongClick(position: Int) { - if (actionMode == null) - actionMode = (activity as AppCompatActivity).startSupportActionMode(this) - - toggleSelection(position) - } - - /** - * Called to toggle selection - * @param position position of selected item - */ - private fun toggleSelection(position: Int) { - adapter.toggleSelection(position) - - val count = adapter.selectedItemCount - if (count == 0) { - actionMode?.finish() - } else { - actionMode?.title = getString(R.string.label_selected, count) - } - } - - /** - * Open chapter in reader - * @param chapter selected chapter - */ - private fun openChapter(item: RecentChapterItem) { - val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) - startActivity(intent) - } - - /** - * Download selected items - * @param chapters list of selected [RecentChapter]s - */ - fun downloadChapters(chapters: List) { - destroyActionModeIfNeeded() - presenter.downloadChapters(chapters) - } - - /** - * Populate adapter with chapters - * @param chapters list of [Any] - */ - fun onNextRecentChapters(chapters: List>) { - (activity as MainActivity).updateEmptyView(chapters.isEmpty(), - R.string.information_no_recent, R.drawable.ic_update_black_128dp) - - destroyActionModeIfNeeded() - adapter.updateDataSet(chapters.toMutableList()) - } - - /** - * Update download status of chapter - * @param download [Download] object containing download progress. - */ - fun onChapterStatusChange(download: Download) { - getHolder(download)?.notifyStatus(download.status) - } - - /** - * Returns holder belonging to chapter - * @param download [Download] object containing download progress. - */ - private fun getHolder(download: Download): RecentChapterHolder? { - return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder - } - - /** - * Mark chapter as read - * @param chapters list of chapters - */ - fun markAsRead(chapters: List) { - presenter.markChapterRead(chapters, true) - if (presenter.preferences.removeAfterMarkedAsRead()) { - deleteChapters(chapters) - } - } - - /** - * Delete selected chapters - * @param chapters list of [RecentChapter] objects - */ - fun deleteChapters(chapters: List) { - destroyActionModeIfNeeded() - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(chapters) - } - - /** - * Destory [ActionMode] if it's shown - */ - fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - /** - * Mark chapter as unread - * @param chapters list of selected [RecentChapter] - */ - fun markAsUnread(chapters: List) { - presenter.markChapterRead(chapters, false) - } - - /** - * Start downloading chapter - * @param chapter selected chapter with manga - */ - fun downloadChapter(chapter: RecentChapterItem) { - presenter.downloadChapters(listOf(chapter)) - } - - /** - * Start deleting chapter - * @param chapter selected chapter with manga - */ - fun deleteChapter(chapter: RecentChapterItem) { - DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) - presenter.deleteChapters(listOf(chapter)) - } - - /** - * Called when chapters are deleted - */ - fun onChaptersDeleted() { - dismissDeletingDialog() - adapter.notifyDataSetChanged() - } - - /** - * Called when error while deleting - * @param error error message - */ - fun onChaptersDeletedError(error: Throwable) { - dismissDeletingDialog() - Timber.e(error) - } - - /** - * Called to dismiss deleting dialog - */ - fun dismissDeletingDialog() { - (childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment) - ?.dismissAllowingStateLoss() - } - - /** - * Called when ActionMode item clicked - * @param mode the ActionMode object - * @param item item from ActionMode. - */ - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - if (!isAdded) return true - - when (item.itemId) { - R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) - R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) - R.id.action_download -> downloadChapters(getSelectedChapters()) - R.id.action_delete -> { - MaterialDialog.Builder(activity) - .content(R.string.confirm_delete_chapters) - .positiveText(android.R.string.yes) - .negativeText(android.R.string.no) - .onPositive { dialog, action -> deleteChapters(getSelectedChapters()) } - .show() - } - else -> return false - } - return true - } - - /** - * Called when ActionMode created. - * @param mode the ActionMode object - * @param menu menu object of ActionMode - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) - adapter.mode = FlexibleAdapter.MODE_MULTI - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - /** - * Called when ActionMode destroyed - * @param mode the ActionMode object - */ - override fun onDestroyActionMode(mode: ActionMode?) { - adapter.mode = FlexibleAdapter.MODE_IDLE - adapter.clearSelection() - actionMode = null - } - +package eu.kanade.tachiyomi.ui.recent_updates + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.view.ActionMode +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import com.jakewharton.rxbinding.support.v4.widget.refreshes +import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.fragment_recent_chapters.view.* +import timber.log.Timber + +/** + * Fragment that shows recent chapters. + * Uses [R.layout.fragment_recent_chapters]. + * UI related actions should be called from here. + */ +class RecentChaptersController : NucleusController(), + NoToolbarElevationController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + FlexibleAdapter.OnUpdateListener, + ConfirmDeleteChaptersDialog.Listener { + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing the recent chapters. + */ + var adapter: RecentChaptersAdapter? = null + private set + + override fun getTitle(): String? { + return resources?.getString(R.string.label_recent_updates) + } + + override fun createPresenter(): RecentChaptersPresenter { + return RecentChaptersPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.fragment_recent_chapters, container, false) + } + + /** + * Called when view is created + * @param view created view + * @param savedViewState status of saved sate + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Init RecyclerView and adapter + val layoutManager = LinearLayoutManager(context) + recycler.layoutManager = layoutManager + recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + recycler.setHasFixedSize(true) + adapter = RecentChaptersAdapter(this@RecentChaptersController) + recycler.adapter = adapter + + recycler.scrollStateChanges().subscribeUntilDestroy { + // Disable swipe refresh when view is not at the top + val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition() + swipe_refresh.isEnabled = firstPos == 0 + } + + swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) + swipe_refresh.refreshes().subscribeUntilDestroy { + if (!LibraryUpdateService.isRunning(context)) { + LibraryUpdateService.start(context) + context.toast(R.string.action_update_library) + } + // It can be a very long operation, so we disable swipe refresh and show a toast. + swipe_refresh.isRefreshing = false + } + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + actionMode = null + } + + /** + * Returns selected chapters + * @return list of selected chapters + */ + fun getSelectedChapters(): List { + val adapter = adapter ?: return emptyList() + return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } + } + + /** + * Called when item in list is clicked + * @param position position of clicked item + */ + override fun onItemClick(position: Int): Boolean { + val adapter = adapter ?: return false + + // Get item from position + val item = adapter.getItem(position) as? RecentChapterItem ?: return false + if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openChapter(item) + return false + } + } + + /** + * Called when item in list is long clicked + * @param position position of clicked item + */ + override fun onItemLongClick(position: Int) { + if (actionMode == null) + actionMode = (activity as AppCompatActivity).startSupportActionMode(this) + + toggleSelection(position) + } + + /** + * Called to toggle selection + * @param position position of selected item + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + adapter.toggleSelection(position) + actionMode?.invalidate() + } + + /** + * Open chapter in reader + * @param chapter selected chapter + */ + private fun openChapter(item: RecentChapterItem) { + val activity = activity ?: return + val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) + startActivity(intent) + } + + /** + * Download selected items + * @param chapters list of selected [RecentChapter]s + */ + fun downloadChapters(chapters: List) { + destroyActionModeIfNeeded() + presenter.downloadChapters(chapters) + } + + /** + * Populate adapter with chapters + * @param chapters list of [Any] + */ + fun onNextRecentChapters(chapters: List>) { + destroyActionModeIfNeeded() + adapter?.updateDataSet(chapters.toMutableList()) + } + + override fun onUpdateEmptyView(size: Int) { + val emptyView = view?.empty_view ?: return + if (size > 0) { + emptyView.hide() + } else { + emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent) + } + } + + /** + * Update download status of chapter + * @param download [Download] object containing download progress. + */ + fun onChapterStatusChange(download: Download) { + getHolder(download)?.notifyStatus(download.status) + } + + /** + * Returns holder belonging to chapter + * @param download [Download] object containing download progress. + */ + private fun getHolder(download: Download): RecentChapterHolder? { + return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder + } + + /** + * Mark chapter as read + * @param chapters list of chapters + */ + fun markAsRead(chapters: List) { + presenter.markChapterRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + deleteChapters(chapters) + } + } + + override fun deleteChapters(chaptersToDelete: List) { + destroyActionModeIfNeeded() + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(chaptersToDelete) + } + + /** + * Destory [ActionMode] if it's shown + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + /** + * Mark chapter as unread + * @param chapters list of selected [RecentChapter] + */ + fun markAsUnread(chapters: List) { + presenter.markChapterRead(chapters, false) + } + + /** + * Start downloading chapter + * @param chapter selected chapter with manga + */ + fun downloadChapter(chapter: RecentChapterItem) { + presenter.downloadChapters(listOf(chapter)) + } + + /** + * Start deleting chapter + * @param chapter selected chapter with manga + */ + fun deleteChapter(chapter: RecentChapterItem) { + DeletingChaptersDialog().showDialog(router) + presenter.deleteChapters(listOf(chapter)) + } + + /** + * Called when chapters are deleted + */ + fun onChaptersDeleted() { + dismissDeletingDialog() + adapter?.notifyDataSetChanged() + } + + /** + * Called when error while deleting + * @param error error message + */ + fun onChaptersDeletedError(error: Throwable) { + dismissDeletingDialog() + Timber.e(error) + } + + /** + * Called to dismiss deleting dialog + */ + fun dismissDeletingDialog() { + router.popControllerWithTag(DeletingChaptersDialog.TAG) + } + + /** + * Called when ActionMode created. + * @param mode the ActionMode object + * @param menu menu object of ActionMode + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) + adapter?.mode = FlexibleAdapter.MODE_MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val count = adapter?.selectedItemCount ?: 0 + if (count == 0) { + // Destroy action mode if there are no items selected. + destroyActionModeIfNeeded() + } else { + mode.title = resources?.getString(R.string.label_selected, count) + } + return false + } + + /** + * Called when ActionMode item clicked + * @param mode the ActionMode object + * @param item item from ActionMode. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_mark_as_read -> markAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters()) + R.id.action_download -> downloadChapters(getSelectedChapters()) + R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters()) + .showDialog(router) + else -> return false + } + return true + } + + /** + * Called when ActionMode destroyed + * @param mode the ActionMode object + */ + override fun onDestroyActionMode(mode: ActionMode?) { + adapter?.mode = FlexibleAdapter.MODE_IDLE + adapter?.clearSelection() + actionMode = null + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt index 8fb4a5b317..fe7cebfb53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt @@ -14,29 +14,18 @@ import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import timber.log.Timber -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.* -class RecentChaptersPresenter : BasePresenter() { - /** - * Used to connect to database - */ - val db: DatabaseHelper by injectLazy() +class RecentChaptersPresenter( + val preferences: PreferencesHelper = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val downloadManager: DownloadManager = Injekt.get(), + private val sourceManager: SourceManager = Injekt.get() +) : BasePresenter() { - /** - * Used to get settings - */ - val preferences: PreferencesHelper by injectLazy() - - /** - * Used to get information from download manager - */ - val downloadManager: DownloadManager by injectLazy() - - /** - * Used to get source from source id - */ - val sourceManager: SourceManager by injectLazy() + private val context = preferences.context /** * List containing chapter and manga information @@ -48,11 +37,11 @@ class RecentChaptersPresenter : BasePresenter() { getRecentChaptersObservable() .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache(RecentChaptersFragment::onNextRecentChapters) + .subscribeLatestCache(RecentChaptersController::onNextRecentChapters) getChapterStatusObservable() - .subscribeLatestCache(RecentChaptersFragment::onChapterStatusChange, - { view, error -> Timber.e(error) }) + .subscribeLatestCache(RecentChaptersController::onChapterStatusChange, + { _, error -> Timber.e(error) }) } /** @@ -207,9 +196,9 @@ class RecentChaptersPresenter : BasePresenter() { .toList() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, result -> + .subscribeFirst({ view, _ -> view.onChaptersDeleted() - }, RecentChaptersFragment::onChaptersDeletedError) + }, RecentChaptersController::onChaptersDeletedError) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt index e95b457ef5..766ce466db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt @@ -1,57 +1,48 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.view.ViewGroup -import eu.davidea.flexibleadapter4.FlexibleAdapter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.util.inflate import uy.kohesive.injekt.injectLazy +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols /** * Adapter of RecentlyReadHolder. * Connection between Fragment and Holder * Holder updates should be called from here. * - * @param fragment a RecentlyReadFragment object + * @param controller a RecentlyReadController object * @constructor creates an instance of the adapter. */ -class RecentlyReadAdapter(val fragment: RecentlyReadFragment) -: FlexibleAdapter() { +class RecentlyReadAdapter(controller: RecentlyReadController) +: FlexibleAdapter(null, controller, true) { val sourceManager by injectLazy() - /** - * Called when ViewHolder is created - * @param parent parent View - * @param viewType int containing viewType - */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentlyReadHolder { - val view = parent.inflate(R.layout.item_recently_read) - return RecentlyReadHolder(view, this) - } + val resumeClickListener: OnResumeClickListener = controller + + val removeClickListener: OnRemoveClickListener = controller + + val coverClickListener: OnCoverClickListener = controller /** - * Called when ViewHolder is bind - * @param holder bind holder - * @param position position of holder + * DecimalFormat used to display correct chapter number */ - override fun onBindViewHolder(holder: RecentlyReadHolder, position: Int) { - val item = getItem(position) - holder.onSetValues(item) + val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols() + .apply { decimalSeparator = '.' }) + + val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + + interface OnResumeClickListener { + fun onResumeClick(position: Int) } - /** - * Update items - * @param items items - */ - fun setItems(items: List) { - mItems = items - notifyDataSetChanged() + interface OnRemoveClickListener { + fun onRemoveClick(position: Int) } - override fun updateDataSet(param: String?) { - // Empty function + interface OnCoverClickListener { + fun onCoverClick(position: Int) } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt new file mode 100644 index 0000000000..2b5c81693f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadController.kt @@ -0,0 +1,134 @@ +package eu.kanade.tachiyomi.ui.recently_read + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.fragment_recently_read.view.* + +/** + * Fragment that shows recently read manga. + * Uses R.layout.fragment_recently_read. + * UI related actions should be called from here. + */ +class RecentlyReadController : NucleusController(), + FlexibleAdapter.OnUpdateListener, + RecentlyReadAdapter.OnRemoveClickListener, + RecentlyReadAdapter.OnResumeClickListener, + RecentlyReadAdapter.OnCoverClickListener, + RemoveHistoryDialog.Listener { + + /** + * Adapter containing the recent manga. + */ + var adapter: RecentlyReadAdapter? = null + private set + + override fun getTitle(): String? { + return resources?.getString(R.string.label_recent_manga) + } + + override fun createPresenter(): RecentlyReadPresenter { + return RecentlyReadPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.fragment_recently_read, container, false) + } + + /** + * Called when view is created + * + * @param view created view + * @param savedViewState saved state of the view + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + with(view) { + // Initialize adapter + recycler.layoutManager = LinearLayoutManager(context) + adapter = RecentlyReadAdapter(this@RecentlyReadController) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + } + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + adapter = null + } + + /** + * Populate adapter with chapters + * + * @param mangaHistory list of manga history + */ + fun onNextManga(mangaHistory: List) { + adapter?.updateDataSet(mangaHistory.toList()) + } + + override fun onUpdateEmptyView(size: Int) { + val emptyView = view?.empty_view ?: return + if (size > 0) { + emptyView.hide() + } else { + emptyView.show(R.drawable.ic_glasses_black_128dp, R.string.information_no_recent_manga) + } + } + + override fun onResumeClick(position: Int) { + val activity = activity ?: return + val adapter = adapter ?: return + if (position == RecyclerView.NO_POSITION) return + + val (manga, chapter, _) = adapter.getItem(position).mch + + val nextChapter = presenter.getNextChapter(chapter, manga) + if (nextChapter != null) { + val intent = ReaderActivity.newIntent(activity, manga, nextChapter) + startActivity(intent) + } else { + activity.toast(R.string.no_next_chapter) + } + } + + override fun onRemoveClick(position: Int) { + val adapter = adapter ?: return + if (position == RecyclerView.NO_POSITION) return + + val (manga, _, history) = adapter.getItem(position).mch + + RemoveHistoryDialog(this, manga, history).showDialog(router) + } + + override fun onCoverClick(position: Int) { + val manga = adapter?.getItem(position)?.mch?.manga ?: return + router.pushController(RouterTransaction.with(MangaController(manga)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + override fun removeHistory(manga: Manga, history: History, all: Boolean) { + if (all) { + // Reset last read of chapter to 0L + presenter.removeAllFromHistory(manga.id!!) + } else { + // Remove all chapters belonging to manga from library + presenter.removeFromHistory(history) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt deleted file mode 100644 index 3930570a41..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package eu.kanade.tachiyomi.ui.recently_read - -import android.os.Bundle -import android.support.v7.widget.LinearLayoutManager -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.toast -import kotlinx.android.synthetic.main.fragment_recently_read.* -import nucleus.factory.RequiresPresenter - -/** - * Fragment that shows recently read manga. - * Uses R.layout.fragment_recently_read. - * UI related actions should be called from here. - */ -@RequiresPresenter(RecentlyReadPresenter::class) -class RecentlyReadFragment : BaseRxFragment() { - companion object { - /** - * Create new RecentChaptersFragment. - */ - fun newInstance(): RecentlyReadFragment { - return RecentlyReadFragment() - } - } - - /** - * Adapter containing the recent manga. - */ - lateinit var adapter: RecentlyReadAdapter - private set - - /** - * Called when view gets created - * - * @param inflater layout inflater - * @param container view group - * @param savedState status of saved state - */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_recently_read, container, false) - } - - /** - * Called when view is created - * - * @param view created view - * @param savedState status of saved sate - */ - override fun onViewCreated(view: View?, savedState: Bundle?) { - // Initialize adapter - recycler.layoutManager = LinearLayoutManager(activity) - adapter = RecentlyReadAdapter(this) - recycler.setHasFixedSize(true) - recycler.adapter = adapter - - // Update toolbar text - setToolbarTitle(R.string.label_recent_manga) - } - - /** - * Populate adapter with chapters - * - * @param mangaHistory list of manga history - */ - fun onNextManga(mangaHistory: List) { - (activity as MainActivity).updateEmptyView(mangaHistory.isEmpty(), - R.string.information_no_recent_manga, R.drawable.ic_glasses_black_128dp) - - adapter.setItems(mangaHistory) - } - - /** - * Reset last read of chapter to 0L - * @param history history belonging to chapter - */ - fun removeFromHistory(history: History) { - presenter.removeFromHistory(history) - } - - /** - * Removes all chapters belonging to manga from library - * @param mangaId id of manga - */ - fun removeAllFromHistory(mangaId: Long) { - presenter.removeAllFromHistory(mangaId) - } - - /** - * Open chapter to continue reading - * @param chapter chapter that is opened - * @param manga manga belonging to chapter - */ - fun openChapter(chapter: Chapter, manga: Manga) { - if (!chapter.read) { - val intent = ReaderActivity.newIntent(activity, manga, chapter) - startActivity(intent) - } else { - presenter.openNextChapter(chapter, manga) - } - } - - /** - * Called from the presenter when wanting to open the next chapter of the current one. - * @param chapter the next chapter or null if it doesn't exist. - * @param manga the manga of the chapter. - */ - fun onOpenNextChapter(chapter: Chapter?, manga: Manga) { - if (chapter == null) { - context.toast(R.string.no_next_chapter) - } - // Avoid crashes if the fragment isn't resumed, the event will be ignored but it's unlikely - // to happen. - else if (isResumed) { - val intent = ReaderActivity.newIntent(activity, manga, chapter) - startActivity(intent) - } - } - - /** - * Open manga info page - * @param manga manga belonging to info page - */ - fun openMangaInfo(manga: Manga) { - val intent = MangaActivity.newIntent(activity, manga, true) - startActivity(intent) - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt index e63723fd09..aec2dacb2c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt @@ -1,17 +1,12 @@ package eu.kanade.tachiyomi.ui.recently_read -import android.support.v7.widget.RecyclerView import android.view.View -import com.afollestad.materialdialogs.MaterialDialog import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.widget.DialogCheckboxView import kotlinx.android.synthetic.main.item_recently_read.view.* -import java.text.DateFormat -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols import java.util.* /** @@ -23,39 +18,47 @@ import java.util.* * @param adapter the adapter handling this holder. * @constructor creates a new recent chapter holder. */ -class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) - : RecyclerView.ViewHolder(view) { +class RecentlyReadHolder( + view: View, + val adapter: RecentlyReadAdapter +) : FlexibleViewHolder(view, adapter) { - /** - * DecimalFormat used to display correct chapter number - */ - private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) + init { + itemView.remove.setOnClickListener { + adapter.removeClickListener.onRemoveClick(adapterPosition) + } - private val df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + itemView.resume.setOnClickListener { + adapter.resumeClickListener.onResumeClick(adapterPosition) + } + + itemView.cover.setOnClickListener { + adapter.coverClickListener.onCoverClick(adapterPosition) + } + } /** * Set values of view * * @param item item containing history information */ - fun onSetValues(item: MangaChapterHistory) { + fun bind(item: MangaChapterHistory) { // Retrieve objects - val manga = item.manga - val chapter = item.chapter - val history = item.history + val (manga, chapter, history) = item // Set manga title itemView.manga_title.text = manga.title // Set source + chapter title - val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) + val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source) .format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber) // Set last read timestamp title - itemView.last_read.text = df.format(Date(history.last_read)) + itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read)) // Set cover + Glide.clear(itemView.cover) if (!manga.thumbnail_url.isNullOrEmpty()) { Glide.with(itemView.context) .load(manga) @@ -64,40 +67,6 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) .into(itemView.cover) } - // Set remove clickListener - itemView.remove.setOnClickListener { - // Create custom view - val dialogCheckboxView = DialogCheckboxView(itemView.context).apply { - setDescription(R.string.dialog_with_checkbox_remove_description) - setOptionDescription(R.string.dialog_with_checkbox_reset) - } - MaterialDialog.Builder(itemView.context) - .title(R.string.action_remove) - .customView(dialogCheckboxView, true) - .positiveText(R.string.action_remove) - .negativeText(android.R.string.cancel) - .onPositive { materialDialog, dialogAction -> - // Check if user wants all chapters reset - if (dialogCheckboxView.isChecked()) { - adapter.fragment.removeAllFromHistory(manga.id!!) - } else { - adapter.fragment.removeFromHistory(history) - } - } - .onNegative { materialDialog, dialogAction -> - materialDialog.dismiss() - }.show() - } - - // Set continue reading clickListener - itemView.resume.setOnClickListener { - adapter.fragment.openChapter(chapter, manga) - } - - // Set open manga info clickListener - itemView.cover.setOnClickListener { - adapter.fragment.openMangaInfo(manga) - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt new file mode 100644 index 0000000000..6d9fc4b243 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadItem.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.recently_read + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.util.inflate + +class RecentlyReadItem(val mch: MangaChapterHistory) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.item_recently_read + } + + override fun createViewHolder(adapter: FlexibleAdapter<*>, + inflater: LayoutInflater, + parent: ViewGroup): RecentlyReadHolder { + + val view = parent.inflate(layoutRes) + return RecentlyReadHolder(view, adapter as RecentlyReadAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: RecentlyReadHolder, + position: Int, + payloads: List?) { + + holder.bind(mch) + } + + override fun equals(other: Any?): Boolean { + if (other is RecentlyReadItem) { + return mch.manga.id == other.mch.manga.id + } + return false + } + + override fun hashCode(): Int { + return mch.manga.id!!.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt index 9f50473a4e..28abe7881d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt @@ -5,11 +5,9 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import rx.Observable import rx.android.schedulers.AndroidSchedulers -import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.util.* @@ -18,7 +16,7 @@ import java.util.* * Contains information and data for fragment. * Observable updates should be called from here. */ -class RecentlyReadPresenter : BasePresenter() { +class RecentlyReadPresenter : BasePresenter() { /** * Used to connect to database @@ -30,22 +28,21 @@ class RecentlyReadPresenter : BasePresenter() { // Used to get a list of recently read manga getRecentMangaObservable() - .subscribeLatestCache({ view, historyList -> - view.onNextManga(historyList) - }) + .subscribeLatestCache(RecentlyReadController::onNextManga) } /** * Get recent manga observable * @return list of history */ - fun getRecentMangaObservable(): Observable> { + fun getRecentMangaObservable(): Observable> { // Set date for recent manga val cal = Calendar.getInstance() cal.time = Date() cal.add(Calendar.MONTH, -1) return db.getRecentManga(cal.time).asRxObservable() + .map { it.map(::RecentlyReadItem) } .observeOn(AndroidSchedulers.mainThread()) } @@ -73,50 +70,39 @@ class RecentlyReadPresenter : BasePresenter() { } /** - * Open the next chapter instead of the current one. + * Retrieves the next chapter of the given one. + * * @param chapter the chapter of the history object. * @param manga the manga of the chapter. */ - fun openNextChapter(chapter: Chapter, manga: Manga) { + fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? { + if (!chapter.read) { + return chapter + } + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } else -> throw NotImplementedError("Unknown sorting method") } - db.getChapters(manga).asRxSingle() - .map { it.sortedWith(Comparator { c1, c2 -> sortFunction(c1, c2) }) } - .map { chapters -> - val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } - when (manga.sorting) { - Manga.SORTING_SOURCE -> { - chapters.getOrNull(currChapterIndex + 1) - } - Manga.SORTING_NUMBER -> { - val chapterNumber = chapter.chapter_number + val chapters = db.getChapters(manga).executeAsBlocking() + .sortedWith(Comparator { c1, c2 -> sortFunction(c1, c2) }) - var nextChapter: Chapter? = null - for (i in (currChapterIndex + 1) until chapters.size) { - val c = chapters[i] - if (c.chapter_number > chapterNumber && - c.chapter_number <= chapterNumber + 1) { + val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } + return when (manga.sorting) { + Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) + Manga.SORTING_NUMBER -> { + val chapterNumber = chapter.chapter_number - nextChapter = c - break - } - } - nextChapter + ((currChapterIndex + 1) until chapters.size) + .map { chapters[it] } + .firstOrNull { it.chapter_number > chapterNumber && + it.chapter_number <= chapterNumber + 1 } - else -> throw NotImplementedError("Unknown sorting method") - } - } - .toObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, chapter -> - view.onOpenNextChapter(chapter, manga) - }, { view, error -> - Timber.e(error) - }) + } + else -> throw NotImplementedError("Unknown sorting method") + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt new file mode 100644 index 0000000000..8385b4ed43 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RemoveHistoryDialog.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.ui.recently_read + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.History +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.widget.DialogCheckboxView + +class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T: RemoveHistoryDialog.Listener { + + private var manga: Manga? = null + + private var history: History? = null + + constructor(target: T, manga: Manga, history: History) : this() { + this.manga = manga + this.history = history + targetController = target + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val activity = activity!! + + // Create custom view + val dialogCheckboxView = DialogCheckboxView(activity).apply { + setDescription(R.string.dialog_with_checkbox_remove_description) + setOptionDescription(R.string.dialog_with_checkbox_reset) + } + + return MaterialDialog.Builder(activity) + .title(R.string.action_remove) + .customView(dialogCheckboxView, true) + .positiveText(R.string.action_remove) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> onPositive(dialogCheckboxView.isChecked()) } + .build() + } + + private fun onPositive(checked: Boolean) { + val target = targetController as? Listener ?: return + val manga = manga ?: return + val history = history ?: return + + target.removeHistory(manga, history, checked) + } + + interface Listener { + fun removeHistory(manga: Manga, history: History, all: Boolean) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt index 186bf112bb..bc872fb140 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt @@ -7,7 +7,6 @@ import android.support.v7.preference.XpPreferenceFragment import android.view.View import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.util.LocaleHelper diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt new file mode 100644 index 0000000000..078bceff44 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/DrawerSwipeCloseListener.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.widget + +import android.support.v4.widget.DrawerLayout +import android.view.View +import android.view.ViewGroup + +class DrawerSwipeCloseListener( + private val drawer: DrawerLayout, + private val navigationView: ViewGroup +) : DrawerLayout.SimpleDrawerListener() { + + override fun onDrawerOpened(drawerView: View) { + if (drawerView == navigationView) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, drawerView) + } + } + + override fun onDrawerClosed(drawerView: View) { + if (drawerView == navigationView) { + drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, drawerView) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt index 6d0044fe84..f644e3fdd5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt @@ -1,11 +1,11 @@ package eu.kanade.tachiyomi.widget -import android.support.v4.view.PagerAdapter import android.view.View import android.view.ViewGroup +import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter import java.util.* -abstract class RecyclerViewPagerAdapter : PagerAdapter() { +abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() { private val pool = Stack() @@ -21,22 +21,16 @@ abstract class RecyclerViewPagerAdapter : PagerAdapter() { protected open fun recycleView(view: View, position: Int) {} - override fun instantiateItem(container: ViewGroup, position: Int): Any { + override fun createView(container: ViewGroup, position: Int): View { val view = if (pool.isNotEmpty()) pool.pop() else createView(container) bindView(view, position) - container.addView(view) return view } - override fun destroyItem(container: ViewGroup, position: Int, obj: Any) { - val view = obj as View + override fun destroyView(container: ViewGroup, position: Int, view: View) { recycleView(view, position) - container.removeView(view) if (recycle) pool.push(view) } - override fun isViewFromObject(view: View, obj: Any): Boolean { - return view === obj - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java b/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java new file mode 100644 index 0000000000..54cb768c3c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java @@ -0,0 +1,281 @@ +/* + * Copyright 2016 Davide Steduto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.kanade.tachiyomi.widget; + +import android.content.Context; +import android.graphics.Color; +import android.support.annotation.ColorInt; +import android.support.annotation.IntDef; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +import eu.davidea.flexibleadapter.FlexibleAdapter; + +/** + * Helper to simplify the Undo operation with FlexibleAdapter. + * + * @author Davide Steduto + * @since 30/04/2016 + */ +@SuppressWarnings("WeakerAccess") +public class UndoHelper extends Snackbar.Callback { + + /** + * Default undo-timeout of 5''. + */ + public static final int UNDO_TIMEOUT = 5000; + /** + * Indicates that the Confirmation Listener (Undo and Delete) will perform a deletion. + */ + public static final int ACTION_REMOVE = 0; + /** + * Indicates that the Confirmation Listener (Undo and Delete) will perform an update. + */ + public static final int ACTION_UPDATE = 1; + + /** + * Annotation interface for Undo actions. + */ + @IntDef({ACTION_REMOVE, ACTION_UPDATE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Action { + } + + @Action + private int mAction = ACTION_REMOVE; + private List mPositions = null; + private Object mPayload = null; + private FlexibleAdapter mAdapter; + private Snackbar mSnackbar = null; + private OnActionListener mActionListener; + private OnUndoListener mUndoListener; + private @ColorInt int mActionTextColor = Color.TRANSPARENT; + + + /** + * Default constructor. + *

By calling this constructor, {@link FlexibleAdapter#setPermanentDelete(boolean)} + * is set {@code false} automatically. + * + * @param adapter the instance of {@code FlexibleAdapter} + * @param undoListener the callback for the Undo and Delete confirmation + */ + public UndoHelper(FlexibleAdapter adapter, OnUndoListener undoListener) { + this.mAdapter = adapter; + this.mUndoListener = undoListener; + adapter.setPermanentDelete(false); + } + + /** + * Sets the payload to inform other linked items about the change in action. + * + * @param payload any non-null user object to notify the parent (the payload will be + * therefore passed to the bind method of the parent ViewHolder), + * pass null to not notify the parent + * @return this object, so it can be chained + */ + public UndoHelper withPayload(Object payload) { + this.mPayload = payload; + return this; + } + + /** + * By default {@link UndoHelper#ACTION_REMOVE} is performed. + * + * @param action the action, one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} + * @param actionListener the listener for the custom action to perform before the deletion + * @return this object, so it can be chained + */ + public UndoHelper withAction(@Action int action, @NonNull OnActionListener actionListener) { + this.mAction = action; + this.mActionListener = actionListener; + return this; + } + + /** + * Sets the text color of the action. + * + * @param color the color for the action button + * @return this object, so it can be chained + */ + public UndoHelper withActionTextColor(@ColorInt int color) { + this.mActionTextColor = color; + return this; + } + + /** + * As {@link #remove(List, View, CharSequence, CharSequence, int)} but with String + * resources instead of CharSequence. + */ + public void remove(List positions, @NonNull View mainView, + @StringRes int messageStringResId, @StringRes int actionStringResId, + @IntRange(from = -1) int undoTime) { + Context context = mainView.getContext(); + remove(positions, mainView, context.getString(messageStringResId), + context.getString(actionStringResId), undoTime); + } + + /** + * Performs the action on the specified positions and displays a SnackBar to Undo + * the operation. To customize the UPDATE event, please set a custom listener with + * {@link #withAction(int, OnActionListener)} method. + *

By default the DELETE action will be performed.

+ * + * @param positions the position to delete or update + * @param mainView the view to find a parent from + * @param message the text to show. Can be formatted text + * @param actionText the action text to display + * @param undoTime How long to display the message. Either {@link Snackbar#LENGTH_SHORT} or + * {@link Snackbar#LENGTH_LONG} or any custom Integer. + * @see #remove(List, View, int, int, int) + */ + @SuppressWarnings("WrongConstant") + public void remove(List positions, @NonNull View mainView, + CharSequence message, CharSequence actionText, + @IntRange(from = -1) int undoTime) { + this.mPositions = positions; + Snackbar snackbar; + if (!mAdapter.isPermanentDelete()) { + snackbar = Snackbar.make(mainView, message, undoTime > 0 ? undoTime + 400 : undoTime) + .setAction(actionText, new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mUndoListener != null) + mUndoListener.onUndoConfirmed(mAction); + } + }); + } else { + snackbar = Snackbar.make(mainView, message, undoTime); + } + if (mActionTextColor != Color.TRANSPARENT) { + snackbar.setActionTextColor(mActionTextColor); + } + mSnackbar = snackbar; + snackbar.addCallback(this); + snackbar.show(); + } + + public void dismissNow() { + if (mSnackbar != null) { + mSnackbar.removeCallback(this); + mSnackbar.dismiss(); + onDismissed(mSnackbar, Snackbar.Callback.DISMISS_EVENT_MANUAL); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onDismissed(Snackbar snackbar, int event) { + if (mAdapter.isPermanentDelete()) return; + switch (event) { + case DISMISS_EVENT_SWIPE: + case DISMISS_EVENT_MANUAL: + case DISMISS_EVENT_TIMEOUT: + if (mUndoListener != null) + mUndoListener.onDeleteConfirmed(mAction); + mAdapter.emptyBin(); + mSnackbar = null; + case DISMISS_EVENT_CONSECUTIVE: + case DISMISS_EVENT_ACTION: + default: + break; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onShown(Snackbar snackbar) { + boolean consumed = false; + // Perform the action before deletion + if (mActionListener != null) consumed = mActionListener.onPreAction(); + // Remove selected items from Adapter list after SnackBar is shown + if (!consumed) mAdapter.removeItems(mPositions, mPayload); + // Perform the action after the deletion + if (mActionListener != null) mActionListener.onPostAction(); + // Here, we can notify the callback only in case of permanent deletion + if (mAdapter.isPermanentDelete() && mUndoListener != null) + mUndoListener.onDeleteConfirmed(mAction); + } + + /** + * Basic implementation of {@link OnActionListener} interface. + *

Override the methods as your convenience.

+ */ + public static class SimpleActionListener implements OnActionListener { + @Override + public boolean onPreAction() { + return false; + } + + @Override + public void onPostAction() { + + } + } + + public interface OnActionListener { + /** + * Performs the custom action before item deletion. + * + * @return true if action has been consumed and should stop the deletion, false to + * continue with the deletion + */ + boolean onPreAction(); + + /** + * Performs custom action After items deletion. Useful to finish the action mode and perform + * secondary custom actions. + */ + void onPostAction(); + } + + /** + * @since 30/04/2016 + */ + public interface OnUndoListener { + /** + * Called when Undo event is triggered. Perform custom action after restoration. + *

Usually for a delete restoration you should call + * {@link FlexibleAdapter#restoreDeletedItems()}.

+ * + * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} + */ + void onUndoConfirmed(int action); + + /** + * Called when Undo timeout is over and action must be committed in the user Database. + *

Due to Java Generic, it's too complicated and not well manageable if we pass the + * List<T> object.
+ * So, to get deleted items, use {@link FlexibleAdapter#getDeletedItems()} from the + * implementation of this method.

+ * + * @param action one of {@link UndoHelper#ACTION_REMOVE}, {@link UndoHelper#ACTION_UPDATE} + */ + void onDeleteConfirmed(int action); + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_edit_categories.xml b/app/src/main/res/layout/activity_edit_categories.xml index 0668bb24ed..770ef3a6d2 100644 --- a/app/src/main/res/layout/activity_edit_categories.xml +++ b/app/src/main/res/layout/activity_edit_categories.xml @@ -2,27 +2,17 @@ - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior" + /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9e24d4df28..92da0a5fc2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,7 +7,8 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> - @@ -32,22 +33,12 @@ - - + android:layout_height="match_parent" /> - - - - + + diff --git a/app/src/main/res/layout/categories_controller.xml b/app/src/main/res/layout/categories_controller.xml new file mode 100644 index 0000000000..8dfa964057 --- /dev/null +++ b/app/src/main/res/layout/categories_controller.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_catalogue.xml b/app/src/main/res/layout/fragment_catalogue.xml index 92d1845d18..48d8601ba8 100644 --- a/app/src/main/res/layout/fragment_catalogue.xml +++ b/app/src/main/res/layout/fragment_catalogue.xml @@ -11,7 +11,7 @@ android:fitsSystemWindows="true" android:orientation="vertical" android:id="@+id/catalogue_view" - tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment"> + tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> diff --git a/app/src/main/res/layout/fragment_recent_chapters.xml b/app/src/main/res/layout/fragment_recent_chapters.xml index b93720dc2e..f1b9e55ebb 100644 --- a/app/src/main/res/layout/fragment_recent_chapters.xml +++ b/app/src/main/res/layout/fragment_recent_chapters.xml @@ -1,5 +1,4 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_recently_read.xml b/app/src/main/res/layout/fragment_recently_read.xml index ce9ba5631e..6ee45a322d 100644 --- a/app/src/main/res/layout/fragment_recently_read.xml +++ b/app/src/main/res/layout/fragment_recently_read.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_track.xml b/app/src/main/res/layout/fragment_track.xml index b73a8cad6b..7a776a10de 100644 --- a/app/src/main/res/layout/fragment_track.xml +++ b/app/src/main/res/layout/fragment_track.xml @@ -11,7 +11,7 @@ android:orientation="vertical"> diff --git a/app/src/main/res/layout/item_library_category2.xml b/app/src/main/res/layout/item_library_category2.xml new file mode 100644 index 0000000000..df6b7ce5ac --- /dev/null +++ b/app/src/main/res/layout/item_library_category2.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/library_controller.xml b/app/src/main/res/layout/library_controller.xml new file mode 100644 index 0000000000..24e3d9af43 --- /dev/null +++ b/app/src/main/res/layout/library_controller.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/manga_controller.xml b/app/src/main/res/layout/manga_controller.xml new file mode 100644 index 0000000000..27421c4c5f --- /dev/null +++ b/app/src/main/res/layout/manga_controller.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file