UI with Conductor (#784)

This commit is contained in:
inorichi 2017-05-06 15:49:39 +02:00 committed by GitHub
parent 89b293fecd
commit 2eeac0bf8b
110 changed files with 7463 additions and 5807 deletions

View File

@ -100,6 +100,16 @@ android {
dependencies { 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 // Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385' compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
compile 'com.github.inorichi:junrar-android:634c1f5' compile 'com.github.inorichi:junrar-android:634c1f5'
@ -212,7 +222,7 @@ dependencies {
} }
buildscript { buildscript {
ext.kotlin_version = '1.1.1' ext.kotlin_version = '1.1.2'
repositories { repositories {
mavenCentral() mavenCentral()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}

View File

@ -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) {}
}

View File

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

View File

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

View File

@ -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
}

View File

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

View File

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

View File

@ -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));
}
}

View File

@ -7,11 +7,15 @@ import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.* import android.support.v7.widget.*
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ProgressBar
import android.widget.Spinner import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference 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.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -19,49 +23,66 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.util.connectivityManager import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_catalogue.* import kotlinx.android.synthetic.main.fragment_catalogue.view.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subscriptions.Subscriptions
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit
/** /**
* Fragment that shows the manga from the catalogue. * Controller to manage the catalogues available in the app.
* Uses R.layout.fragment_catalogue.
*/ */
@RequiresPresenter(CataloguePresenter::class) open class CatalogueController(bundle: Bundle? = null) :
open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener<ProgressItem> { FlexibleAdapter.EndlessScrollListener<ProgressItem>,
ChangeMangaCategoriesDialog.Listener {
/** /**
* Preferences helper. * Preferences helper.
*/ */
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/** /**
* Spinner shown in the toolbar to change the selected source. * Spinner shown in the toolbar to change the selected source.
*/ */
private var spinner: Spinner? = null private var spinner: Spinner? = null
/** /**
* Adapter containing the list of manga from the catalogue. * Snackbar containing an error message when a request fails.
*/ */
private lateinit var adapter: FlexibleAdapter<IFlexible<*>> 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. * Query of the search box.
@ -75,113 +96,57 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
private var selectedIndex: Int = 0 private var selectedIndex: Int = 0
/** /**
* Time in milliseconds to wait for input events in the search query before doing network calls. * Subscription for the search view.
*/ */
private val SEARCH_TIMEOUT = 1000L private var searchViewSubscription: Subscription? = null
/**
* Subject to debounce the query.
*/
private val queryDebouncerSubject = PublishSubject.create<String>()
/**
* Subscription of the debouncer subject.
*/
private var queryDebouncerSubscription: Subscription? = null
/**
* Subscription of the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null 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 private var progressItem: ProgressItem? = null
companion object { init {
/**
* 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) setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { 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) return inflater.inflate(R.layout.fragment_catalogue, container, false)
} }
override fun onViewCreated(view: View, savedState: Bundle?) { override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
// Initialize adapter, scroll listener and recycler views // Initialize adapter, scroll listener and recycler views
adapter = FlexibleAdapter(null, this) adapter = FlexibleAdapter(null, this)
setupRecycler() setupRecycler(view)
// Create toolbar spinner // Create toolbar spinner
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext ?: activity val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
?: activity
val spinnerAdapter = ArrayAdapter(themedContext, val spinnerAdapter = ArrayAdapter(themedContext,
android.R.layout.simple_spinner_item, presenter.sources) android.R.layout.simple_spinner_item, presenter.sources)
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item) spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
val onItemSelected = IgnoreFirstSpinnerListener { position -> val onItemSelected: (Int) -> Unit = { position ->
val source = spinnerAdapter.getItem(position) val source = spinnerAdapter.getItem(position)
if (!presenter.isValidSource(source)) { if (!presenter.isValidSource(source)) {
spinner?.setSelection(selectedIndex) spinner?.setSelection(selectedIndex)
context.toast(R.string.source_requires_login) activity?.toast(R.string.source_requires_login)
} else if (source != presenter.source) { } else if (source != presenter.source) {
selectedIndex = position selectedIndex = position
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.setActiveSource(source) presenter.setActiveSource(source)
navView?.setFilters(presenter.filterItems) navView?.setFilters(presenter.filterItems)
activity.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
} }
@ -190,28 +155,47 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
spinner = Spinner(themedContext).apply { spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter adapter = spinnerAdapter
setSelection(selectedIndex) setSelection(selectedIndex)
onItemSelectedListener = onItemSelected itemSelections()
.skip(1)
.filter { it != AdapterView.INVALID_POSITION }
.subscribeUntilDestroy { onItemSelected(it) }
} }
setToolbarTitle("") activity?.toolbar?.addView(spinner)
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 // Inflate and prepare drawer
val navView = activity.drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
this.navView = navView this.navView = navView
activity.drawer.addView(navView) drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
activity.drawer.addDrawerListener(drawerListener) drawer.addDrawerListener(it)
}
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
navView.post { navView.post {
if (isAdded && !activity.drawer.isDrawerOpen(navView)) if (isAttached && !drawer.isDrawerOpen(navView))
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
} }
navView.onSearchClicked = { navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
} }
@ -221,36 +205,39 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
presenter.sourceFilters = newFilters presenter.sourceFilters = newFilters
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
} }
return navView
showProgressBar()
} }
private fun setupRecycler() { override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
if (!isAdded) return drawerListener?.let { drawer.removeDrawerListener(it) }
drawerListener = null
navView = null
}
private fun setupRecycler(view: View) {
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
val oldRecycler = catalogue_view.getChildAt(1)
var oldPosition = RecyclerView.NO_POSITION var oldPosition = RecyclerView.NO_POSITION
val oldRecycler = view.catalogue_view?.getChildAt(1)
if (oldRecycler is RecyclerView) { if (oldRecycler is RecyclerView) {
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
oldRecycler.adapter = null oldRecycler.adapter = null
catalogue_view.removeView(oldRecycler) view.catalogue_view?.removeView(oldRecycler)
} }
recycler = if (presenter.isListMode) { val recycler = if (presenter.isListMode) {
RecyclerView(context).apply { RecyclerView(view.context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
} }
} else { } else {
(catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply { (view.catalogue_view.inflate(R.layout.recycler_autofit) as AutofitRecyclerView).apply {
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
.doOnNext { spanCount = it } .doOnNext { spanCount = it }
.skip(1) .skip(1)
// Set again the adapter to recalculate the covers height // Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueFragment.adapter } .subscribe { adapter = this@CatalogueController.adapter }
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { (layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
@ -265,18 +252,19 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
catalogue_view.addView(recycler, 1) view.catalogue_view.addView(recycler, 1)
if (oldPosition != RecyclerView.NO_POSITION) { if (oldPosition != RecyclerView.NO_POSITION) {
recycler.layoutManager.scrollToPosition(oldPosition) recycler.layoutManager.scrollToPosition(oldPosition)
} }
this.recycler = recycler
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.catalogue_list, menu) inflater.inflate(R.menu.catalogue_list, menu)
// Initialize search menu // Initialize search menu
searchItem = menu.findItem(R.id.action_search).apply { menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView val searchView = actionView as SearchView
if (!query.isBlank()) { if (!query.isBlank()) {
@ -284,17 +272,24 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
searchView.setQuery(query, true) searchView.setQuery(query, true)
searchView.clearFocus() searchView.clearFocus()
} }
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
onSearchEvent(query, true)
return true
}
override fun onQueryTextChange(newText: String): Boolean { val searchEventsObservable = searchView.queryTextChangeEvents()
onSearchEvent(newText, false) .skip(1)
return true .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 // Setup filters button
@ -322,51 +317,12 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode() R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity.drawer.openDrawer(Gravity.END) } R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
return true 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. * Restarts the request with a new query.
* *
@ -378,7 +334,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
return return
showProgressBar() showProgressBar()
adapter.clear() adapter?.clear()
presenter.restartPager(newQuery) presenter.restartPager(newQuery)
} }
@ -390,6 +346,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param mangas the list of manga of the page. * @param mangas the list of manga of the page.
*/ */
fun onAddPage(page: Int, mangas: List<CatalogueItem>) { fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
val adapter = adapter ?: return
hideProgressBar() hideProgressBar()
if (page == 1) { if (page == 1) {
adapter.clear() adapter.clear()
@ -404,13 +361,15 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param error the error received. * @param error the error received.
*/ */
fun onAddPageError(error: Throwable) { fun onAddPageError(error: Throwable) {
Timber.e(error)
val adapter = adapter ?: return
adapter.onLoadMoreComplete(null) adapter.onLoadMoreComplete(null)
hideProgressBar() hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "") val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss() snack?.dismiss()
snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) { snack = view?.catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar. // If not the first page, show bottom progress bar.
if (adapter.mainItemCount > 0) { if (adapter.mainItemCount > 0) {
@ -429,19 +388,20 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
*/ */
private fun resetProgressItem() { private fun resetProgressItem() {
progressItem = ProgressItem() progressItem = ProgressItem()
adapter.endlessTargetCount = 0 adapter?.endlessTargetCount = 0
adapter.setEndlessScrollListener(this, progressItem!!) adapter?.setEndlessScrollListener(this, progressItem!!)
} }
/** /**
* Called by the adapter when scrolled near the bottom. * Called by the adapter when scrolled near the bottom.
*/ */
override fun onLoadMore(lastPosition: Int, currentPage: Int) { override fun onLoadMore(lastPosition: Int, currentPage: Int) {
Timber.e("onLoadMore")
if (presenter.hasNextPage()) { if (presenter.hasNextPage()) {
presenter.requestNext() presenter.requestNext()
} else { } else {
adapter.onLoadMoreComplete(null) adapter?.onLoadMoreComplete(null)
adapter.endlessTargetCount = 1 adapter?.endlessTargetCount = 1
} }
} }
@ -461,13 +421,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Swaps the current display mode. * Swaps the current display mode.
*/ */
fun swapDisplayMode() { fun swapDisplayMode() {
if (!isAdded) return val view = view ?: return
val adapter = adapter ?: return
presenter.swapDisplayMode() presenter.swapDisplayMode()
val isListMode = presenter.isListMode val isListMode = presenter.isListMode
activity.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
setupRecycler() setupRecycler(view)
if (!isListMode || !context.connectivityManager.isActiveNetworkMetered) { if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
// Initialize mangas if going to grid view or if over wifi when going to list view // Initialize mangas if going to grid view or if over wifi when going to list view
val mangas = (0..adapter.itemCount-1).mapNotNull { val mangas = (0..adapter.itemCount-1).mapNotNull {
(adapter.getItem(it) as? CatalogueItem)?.manga (adapter.getItem(it) as? CatalogueItem)?.manga
@ -482,7 +443,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return the preference. * @return the preference.
*/ */
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns() presenter.prefs.portraitColumns()
else else
presenter.prefs.landscapeColumns() presenter.prefs.landscapeColumns()
@ -495,6 +456,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return the holder of the manga or null if it's not bound. * @return the holder of the manga or null if it's not bound.
*/ */
private fun getHolder(manga: Manga): CatalogueHolder? { private fun getHolder(manga: Manga): CatalogueHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder -> adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
if (item != null && item.manga.id!! == manga.id!!) { if (item != null && item.manga.id!! == manga.id!!) {
@ -509,7 +472,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Shows the progress bar. * Shows the progress bar.
*/ */
private fun showProgressBar() { private fun showProgressBar() {
progress.visibility = ProgressBar.VISIBLE view?.progress?.visible()
snack?.dismiss() snack?.dismiss()
snack = null snack = null
} }
@ -518,7 +481,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* Hides active progress bars. * Hides active progress bars.
*/ */
private fun hideProgressBar() { private fun hideProgressBar() {
progress.visibility = ProgressBar.GONE view?.progress?.gone()
} }
/** /**
@ -528,10 +491,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @return true if the item should be selected, false otherwise. * @return true if the item should be selected, false otherwise.
*/ */
override fun onItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
val item = adapter.getItem(position) as? CatalogueItem ?: return false val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(RouterTransaction.with(MangaController(item.manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
val intent = MangaActivity.newIntent(activity, item.manga, true)
startActivity(intent)
return false return false
} }
@ -545,65 +509,50 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(),
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
// Get manga val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
val manga = (adapter.getItem(position) as? CatalogueItem?)?.manga ?: return if (manga.favorite) {
// Fetch categories MaterialDialog.Builder(activity!!)
val categories = presenter.getCategories() .items(resources?.getString(R.string.remove_from_library))
if (manga.favorite){
MaterialDialog.Builder(activity)
.items(getString(R.string.remove_from_library ))
.itemsCallback { _, _, which, _ -> .itemsCallback { _, _, which, _ ->
when (which) { when (which) {
0 -> { 0 -> {
presenter.changeMangaFavorite(manga) presenter.changeMangaFavorite(manga)
adapter.notifyItemChanged(position) adapter?.notifyItemChanged(position)
} }
} }
}.show() }.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 { } else {
MaterialDialog.Builder(activity) presenter.changeMangaFavorite(manga)
.title(R.string.action_move_category) adapter?.notifyItemChanged(position)
.items(categories.map { it.name })
.itemsCallbackMultiChoice(presenter.getMangaCategoryIds(manga)) { dialog, position, _ -> val categories = presenter.getCategories()
if (position.contains(0) && position.count() > 1) { val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
// Deselect default category if (defaultCategory != null) {
dialog.setSelectedIndices(position.filter {it > 0}.toTypedArray()) presenter.moveMangaToCategory(manga, defaultCategory)
dialog.context.toast(R.string.invalid_combination) } else if (categories.size <= 1) { // default or the one from the user
} presenter.moveMangaToCategory(manga, categories.firstOrNull())
true } else {
} val ids = presenter.getMangaCategoryIds(manga)
.alwaysCallMultiChoiceCallback() val preselected = ids.mapNotNull { id ->
.positiveText(android.R.string.ok) categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
.negativeText(android.R.string.cancel) }.toTypedArray()
.onPositive { dialog, _ ->
val selectedCategories = dialog.selectedIndices?.map { categories[it] } ?: emptyList() ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
updateMangaCategories(manga, selectedCategories, position) .showDialog(router)
}
.build()
.show()
} }
} }
} }
/** /**
* Update manga to use selected categories. * Update manga to use selected categories.
* *
* @param manga needed to change * @param mangas The list of manga to move to categories.
* @param selectedCategories selected categories * @param categories The list of categories where manga will be placed.
* @param position position of adapter
*/ */
private fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>, position: Int) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.updateMangaCategories(manga,selectedCategories) val manga = mangas.firstOrNull() ?: return
adapter.notifyItemChanged(position) presenter.updateMangaCategories(manga, categories)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,14 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Category
import kotlinx.android.synthetic.main.item_edit_categories.view.* import kotlinx.android.synthetic.main.item_edit_categories.view.*
/** /**
* Holder that contains category item. * Holder used to display category items.
* Uses R.layout.item_edit_categories.
* UI related actions should be called from here.
* *
* @param view view of category item. * @param view The view used by category items.
* @param adapter adapter belonging to holder. * @param adapter The adapter containing this holder.
*
* @constructor Create CategoryHolder object
*/ */
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) { 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) { fun bind(category: Category) {
// Set capitalized title. // 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 { private fun getRound(text: String): TextDrawable {
val size = Math.min(itemView.image.width, itemView.image.height) 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)) .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) { override fun onItemReleased(position: Int) {
super.onItemReleased(position) super.onItemReleased(position)
adapter.onItemReleased() adapter.onItemReleaseListener.onItemReleased(position)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
}
}

View File

@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
* *
* @constructor creates an instance of the adapter. * @constructor creates an instance of the adapter.
*/ */
class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() { class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
/** /**
* The categories to bind in the adapter. * The categories to bind in the adapter.
@ -32,8 +32,8 @@ class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerA
* @return a new view. * @return a new view.
*/ */
override fun createView(container: ViewGroup): View { override fun createView(container: ViewGroup): View {
val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView val view = container.inflate(R.layout.item_library_category2) as LibraryCategoryView
view.onCreate(fragment) view.onCreate(controller)
return view return view
} }

View File

@ -1,113 +1,31 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.Gravity import eu.davidea.flexibleadapter.FlexibleAdapter
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.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. * Adapter storing a list of manga in a certain category.
* *
* @param fragment the fragment containing this adapter. * @param view the fragment containing this adapter.
*/ */
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) : class LibraryCategoryAdapter(view: LibraryCategoryView) :
FlexibleAdapter<LibraryHolder, Manga>() { FlexibleAdapter<LibraryItem>(null, view, true) {
/** /**
* The list of manga in this category. * The list of manga in this category.
*/ */
private var mangas: List<Manga> = emptyList() private var mangas: List<LibraryItem> = emptyList()
init {
setHasStableIds(true)
}
/** /**
* Sets a list of manga in the adapter. * Sets a list of manga in the adapter.
* *
* @param list the list to set. * @param list the list to set.
*/ */
fun setItems(list: List<Manga>) { fun setItems(list: List<LibraryItem>) {
mItems = list
// A copy of manga always unfiltered. // A copy of manga always unfiltered.
mangas = ArrayList(list) mangas = list.toList()
updateDataSet(null)
}
/** performFilter()
* 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)
} }
/** /**
@ -116,7 +34,11 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
* @param manga the manga to find. * @param manga the manga to find.
*/ */
fun indexOf(manga: Manga): Int { fun indexOf(manga: Manga): Int {
return mangas.orEmpty().indexOfFirst { it.id == manga.id } return mangas.indexOfFirst { it.manga.id == manga.id }
}
fun performFilter() {
updateDataSet(mangas.filter { it.filter(searchText) })
} }
} }

View File

@ -5,28 +5,29 @@ import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout import android.widget.FrameLayout
import eu.davidea.flexibleadapter4.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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.inflate
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.item_library_category.view.* import kotlinx.android.synthetic.main.item_library_category.view.*
import rx.Subscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
* Fragment containing the library manga for a certain category. * Fragment containing the library manga for a certain category.
* Uses R.layout.fragment_library_category. * Uses R.layout.fragment_library_category.
*/ */
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener { FrameLayout(context, attrs),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener {
/** /**
* Preferences. * Preferences.
@ -36,7 +37,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
/** /**
* The fragment containing this view. * The fragment containing this view.
*/ */
private lateinit var fragment: LibraryFragment private lateinit var controller: LibraryController
/** /**
* Category for this view. * Category for this view.
@ -55,22 +56,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private lateinit var adapter: LibraryCategoryAdapter private lateinit var adapter: LibraryCategoryAdapter
/** /**
* Subscription for the library manga. * Subscriptions while the view is bound.
*/ */
private var libraryMangaSubscription: Subscription? = null private var subscriptions = CompositeSubscription()
/** fun onCreate(controller: LibraryController) {
* Subscription of the library search. this.controller = controller
*/
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()) { recycler = if (preferences.libraryAsList().getOrDefault()) {
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply { (swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
@ -78,7 +69,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
} else { } else {
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply { (swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
spanCount = fragment.mangaPerRow spanCount = controller.mangaPerRow
} }
} }
@ -112,35 +103,32 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun onBind(category: Category) { fun onBind(category: Category) {
this.category = category this.category = category
val presenter = fragment.presenter adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
searchSubscription = presenter.searchSubject.subscribe { text ->
adapter.searchText = text
adapter.updateDataSet()
}
adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
FlexibleAdapter.MODE_MULTI FlexibleAdapter.MODE_MULTI
} else { } else {
FlexibleAdapter.MODE_SINGLE FlexibleAdapter.MODE_SINGLE
} }
libraryMangaSubscription = presenter.libraryMangaSubject subscriptions += controller.searchRelay
.doOnNext { adapter.searchText = it }
.skip(1)
.subscribe { adapter.performFilter() }
subscriptions += controller.libraryMangaRelay
.subscribe { onNextLibraryManga(it) } .subscribe { onNextLibraryManga(it) }
selectionSubscription = presenter.selectionSubject subscriptions += controller.selectionRelay
.subscribe { onSelectionChanged(it) } .subscribe { onSelectionChanged(it) }
} }
fun onRecycle() { fun onRecycle() {
adapter.setItems(emptyList()) adapter.setItems(emptyList())
adapter.clearSelection() adapter.clearSelection()
subscriptions.clear()
} }
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
searchSubscription?.unsubscribe() subscriptions.clear()
libraryMangaSubscription?.unsubscribe()
selectionSubscription?.unsubscribe()
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
@ -158,7 +146,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
adapter.setItems(mangaForCategory) adapter.setItems(mangaForCategory)
if (adapter.mode == FlexibleAdapter.MODE_MULTI) { if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
fragment.presenter.selectedMangas.forEach { manga -> controller.selectedMangas.forEach { manga ->
val position = adapter.indexOf(manga) val position = adapter.indexOf(manga)
if (position != -1 && !adapter.isSelected(position)) { if (position != -1 && !adapter.isSelected(position)) {
adapter.toggleSelection(position) adapter.toggleSelection(position)
@ -184,7 +172,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
is LibrarySelectionEvent.Unselected -> { is LibrarySelectionEvent.Unselected -> {
findAndToggleSelection(event.manga) findAndToggleSelection(event.manga)
if (fragment.presenter.selectedMangas.isEmpty()) { if (controller.selectedMangas.isEmpty()) {
adapter.mode = FlexibleAdapter.MODE_SINGLE adapter.mode = FlexibleAdapter.MODE_SINGLE
} }
} }
@ -214,14 +202,14 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
* @param position the position of the element clicked. * @param position the position of the element clicked.
* @return true if the item should be selected, false otherwise. * @return true if the item should be selected, false otherwise.
*/ */
override fun onListItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
// If the action mode is created and the position is valid, toggle the selection. // If the action mode is created and the position is valid, toggle the selection.
val item = adapter.getItem(position) ?: return false val item = adapter.getItem(position) ?: return false
if (adapter.mode == FlexibleAdapter.MODE_MULTI) { if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
toggleSelection(position) toggleSelection(position)
return true return true
} else { } else {
openManga(item) openManga(item.manga)
return false return false
} }
} }
@ -231,8 +219,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
* *
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onListItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
fragment.createActionModeIfNeeded() controller.createActionModeIfNeeded()
toggleSelection(position) toggleSelection(position)
} }
@ -242,25 +230,19 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
* @param manga the manga to open. * @param manga the manga to open.
*/ */
private fun openManga(manga: Manga) { private fun openManga(manga: Manga) {
// Notify the presenter a manga is being opened. controller.openManga(manga)
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. * Tells the presenter to toggle the selection for the given position.
* *
* @param position the position to toggle. * @param position the position to toggle.
*/ */
private fun toggleSelection(position: Int) { private fun toggleSelection(position: Int) {
val manga = adapter.getItem(position) ?: return val item = adapter.getItem(position) ?: return
fragment.presenter.setSelection(manga, !adapter.isSelected(position)) controller.setSelection(item.manga, !adapter.isSelected(position))
fragment.invalidateActionMode() controller.invalidateActionMode()
} }
} }

View File

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

View File

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

View File

@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.* import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
/** /**
@ -16,10 +16,10 @@ import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
* @param listener a listener to react to single tap and long tap events. * @param listener a listener to react to single tap and long tap events.
* @constructor creates a new library holder. * @constructor creates a new library holder.
*/ */
class LibraryGridHolder(private val view: View, class LibraryGridHolder(
private val adapter: LibraryCategoryAdapter, private val view: View,
listener: FlexibleViewHolder.OnListItemClickListener) private val adapter: FlexibleAdapter<*>
: LibraryHolder(view, adapter, listener) { ) : LibraryHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga 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. * Generic class used to hold the displayed data of a manga in the library.
@ -11,10 +12,10 @@ import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
* @param listener a listener to react to the single tap and long tap events. * @param listener a listener to react to the single tap and long tap events.
*/ */
abstract class LibraryHolder(private val view: View, abstract class LibraryHolder(
adapter: LibraryCategoryAdapter, view: View,
listener: FlexibleViewHolder.OnListItemClickListener) adapter: FlexibleAdapter<*>
: FlexibleViewHolder(view, adapter, listener) { ) : FlexibleViewHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

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

View File

@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.* import kotlinx.android.synthetic.main.item_catalogue_list.view.*
/** /**
@ -17,10 +17,10 @@ import kotlinx.android.synthetic.main.item_catalogue_list.view.*
* @constructor creates a new library holder. * @constructor creates a new library holder.
*/ */
class LibraryListHolder(private val view: View, class LibraryListHolder(
private val adapter: LibraryCategoryAdapter, private val view: View,
listener: FlexibleViewHolder.OnListItemClickListener) private val adapter: FlexibleAdapter<*>
: LibraryHolder(view, adapter, listener) { ) : LibraryHolder(view, adapter) {
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this

View File

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

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import android.util.Pair import android.util.Pair
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@ -23,65 +22,30 @@ import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
/** /**
* Presenter of [LibraryFragment]. * Presenter of [LibraryController].
*/ */
class LibraryPresenter : BasePresenter<LibraryFragment>() { class LibraryPresenter(
private val db: DatabaseHelper = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get()
) : BasePresenter<LibraryController>() {
/** private val context = preferences.context
* 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. * Categories of the library.
*/ */
var categories: List<Category> = emptyList() var categories: List<Category> = emptyList()
private set
/**
* Currently selected manga.
*/
val selectedMangas = mutableListOf<Manga>()
/**
* Search query of the library.
*/
val searchSubject: BehaviorRelay<String> = BehaviorRelay.create()
/**
* Subject to notify the library's viewpager for updates.
*/
val libraryMangaSubject: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
/**
* Subject to notify the UI of selection updates.
*/
val selectionSubject: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
/** /**
* Relay used to apply the UI filters to the last emission of the library. * Relay used to apply the UI filters to the last emission of the library.
@ -110,9 +74,10 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
if (librarySubscription.isNullOrUnsubscribed()) { if (librarySubscription.isNullOrUnsubscribed()) {
librarySubscription = getLibraryObservable() librarySubscription = getLibraryObservable()
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()), .combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
{ lib, tick -> Pair(lib.first, applyFilters(lib.second)) }) { lib, _ -> Pair(lib.first, applyFilters(lib.second)) })
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()), .combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
{ lib, tick -> Pair(lib.first, applySort(lib.second)) }) { lib, _ -> Pair(lib.first, applySort(lib.second)) })
.map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache({ view, pair -> .subscribeLatestCache({ view, pair ->
view.onNextLibraryUpdate(pair.first, pair.second) view.onNextLibraryUpdate(pair.first, pair.second)
@ -265,30 +230,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
librarySubscription?.let { remove(it) } 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. * Returns the common categories for the given list of manga.
* *
@ -304,11 +245,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
/** /**
* Remove the selected manga from the library. * Remove the selected manga from the library.
* *
* @param mangas the list of manga to delete.
* @param deleteChapters whether to also delete downloaded chapters. * @param deleteChapters whether to also delete downloaded chapters.
*/ */
fun removeMangaFromLibrary(deleteChapters: Boolean) { fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
// Create a set of the list // Create a set of the list
val mangaToDelete = selectedMangas.distinctBy { it.id } val mangaToDelete = mangas.distinctBy { it.id }
mangaToDelete.forEach { it.favorite = false } mangaToDelete.forEach { it.favorite = false }
Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() } Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }

View File

@ -1,29 +1,46 @@
package eu.kanade.tachiyomi.ui.main package eu.kanade.tachiyomi.ui.main
import android.animation.ObjectAnimator
import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.TaskStackBuilder
import android.support.v4.view.GravityCompat import android.support.v4.view.GravityCompat
import android.view.MenuItem 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.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment 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.download.DownloadActivity
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.library.LibraryFragment import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.ui.setting.SettingsActivity import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private lateinit var router: Router
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
private var drawerArrow: DrawerArrowDrawable? = null
private var secondaryDrawer: ViewGroup? = null
private val startScreenId by lazy { private val startScreenId by lazy {
when (preferences.startScreen()) { when (preferences.startScreen()) {
1 -> R.id.nav_drawer_library 1 -> R.id.nav_drawer_library
@ -33,9 +50,11 @@ class MainActivity : BaseActivity() {
} }
} }
override fun onCreate(savedState: Bundle?) { private val tabAnimator by lazy { TabsAnimator(tabs) }
override fun onCreate(savedInstanceState: Bundle?) {
setAppTheme() setAppTheme()
super.onCreate(savedState) super.onCreate(savedInstanceState)
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) { if (!isTaskRoot) {
@ -43,29 +62,29 @@ class MainActivity : BaseActivity() {
return return
} }
// Inflate activity_main.xml.
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// Handle Toolbar setSupportActionBar(toolbar)
setupToolbar(toolbar, backNavigation = false)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp) drawerArrow = DrawerArrowDrawable(this)
drawerArrow?.color = Color.WHITE
toolbar.navigationIcon = drawerArrow
// Set behavior of Navigation drawer // Set behavior of Navigation drawer
nav_view.setNavigationItemSelectedListener { item -> nav_view.setNavigationItemSelectedListener { item ->
// Make information view invisible
empty_view.hide()
val id = item.itemId val id = item.itemId
val oldFragment = supportFragmentManager.findFragmentById(R.id.frame_container) val currentRoot = router.backstack.firstOrNull()
if (oldFragment == null || oldFragment.tag.toInt() != id) { if (currentRoot?.tag()?.toIntOrNull() != id) {
when (id) { when (id) {
R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id) R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id) R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id) R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), 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_downloads -> {
startActivity(Intent(this, DownloadActivity::class.java))
}
R.id.nav_drawer_settings -> { R.id.nav_drawer_settings -> {
val intent = Intent(this, SettingsActivity::class.java) val intent = Intent(this, SettingsActivity::class.java)
startActivityForResult(intent, REQUEST_OPEN_SETTINGS) startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
@ -76,44 +95,127 @@ class MainActivity : BaseActivity() {
true true
} }
if (savedState == null) { val container = findViewById(R.id.controller_container) as ViewGroup
router = Conductor.attachRouter(this, container, savedInstanceState)
if (!router.hasRootController()) {
// Set start screen // Set start screen
when (intent.action) { when (intent.action) {
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library) SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates) SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read) SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues) SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
SHORTCUT_MANGA -> router.setRoot(
RouterTransaction.with(MangaController(intent.extras)))
else -> setSelectedDrawerItem(startScreenId) 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 // Show changelog if needed
ChangelogDialogFragment.show(this, preferences, supportFragmentManager) ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
} }
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onDestroy() {
when (item.itemId) { super.onDestroy()
android.R.id.home -> drawer.openDrawer(GravityCompat.START) nav_view?.setNavigationItemSelectedListener(null)
else -> return super.onOptionsItemSelected(item) toolbar?.setNavigationOnClickListener(null)
}
return true
} }
override fun onBackPressed() { override fun onBackPressed() {
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container) val backstackSize = router.backstackSize
if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) { if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
drawer.closeDrawers() drawer.closeDrawers()
} else if (fragment != null && fragment.tag.toInt() != startScreenId) { } else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
if (resumed) {
setSelectedDrawerItem(startScreenId) setSelectedDrawerItem(startScreenId)
} } else if (backstackSize == 1 || !router.handleBack()) {
} else {
super.onBackPressed() 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) { if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) {
if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) { if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) {
@ -132,23 +234,6 @@ class MainActivity : BaseActivity() {
} }
} }
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 { companion object {
private const val REQUEST_OPEN_SETTINGS = 200 private const val REQUEST_OPEN_SETTINGS = 200
// Shortcut actions // Shortcut actions
@ -156,5 +241,7 @@ class MainActivity : BaseActivity() {
private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" 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_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES" private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
} }
} }

View File

@ -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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,11 +27,18 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem
return R.layout.item_chapter return R.layout.item_chapter
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder { override fun createViewHolder(adapter: FlexibleAdapter<*>,
inflater: LayoutInflater,
parent: ViewGroup): ChapterHolder {
return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter) return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>,
holder: ChapterHolder,
position: Int,
payloads: List<Any?>?) {
holder.bind(this, manga) holder.bind(this, manga)
} }

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.manga.chapter package eu.kanade.tachiyomi.ui.manga.chapter
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -10,12 +11,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source 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.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.isNullOrUnsubscribed
import eu.kanade.tachiyomi.util.syncChaptersWithSource import eu.kanade.tachiyomi.util.syncChaptersWithSource
import rx.Observable import rx.Observable
@ -23,44 +19,23 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Presenter of [ChaptersFragment]. * Presenter of [ChaptersController].
*/ */
class ChaptersPresenter : BasePresenter<ChaptersFragment>() { class ChaptersPresenter(
val manga: Manga,
val source: Source,
private val chapterCountRelay: BehaviorRelay<Int>,
private val mangaFavoriteRelay: PublishRelay<Boolean>,
val preferences: PreferencesHelper = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get()
) : BasePresenter<ChaptersController>() {
/** private val context = preferences.context
* 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. * List of chapters of the manga. It's always unfiltered and unsorted.
@ -93,16 +68,10 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) 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. // Prepare the relay.
chaptersRelay.flatMap { applyChapterFilters(it) } chaptersRelay.flatMap { applyChapterFilters(it) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(ChaptersFragment::onNextChapters, .subscribeLatestCache(ChaptersController::onNextChapters,
{ _, error -> Timber.e(error) }) { _, error -> Timber.e(error) })
// Add the subscription that retrieves the chapters from the database, keeps subscribed to // Add the subscription that retrieves the chapters from the database, keeps subscribed to
@ -123,7 +92,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
observeDownloads() observeDownloads()
// Emit the number of chapters to the info tab. // Emit the number of chapters to the info tab.
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size) chapterCountRelay.call(chapters.size)
} }
.subscribe { chaptersRelay.call(it) }) .subscribe { chaptersRelay.call(it) })
} }
@ -134,7 +103,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.filter { download -> download.manga.id == manga.id } .filter { download -> download.manga.id == manga.id }
.doOnNext { onDownloadStatusChange(it) } .doOnNext { onDownloadStatusChange(it) }
.subscribeLatestCache(ChaptersFragment::onChapterStatusChange, .subscribeLatestCache(ChaptersController::onChapterStatusChange,
{ _, error -> Timber.e(error) }) { _, error -> Timber.e(error) })
} }
@ -183,7 +152,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, _ -> .subscribeFirst({ view, _ ->
view.onFetchChaptersDone() view.onFetchChaptersDone()
}, ChaptersFragment::onFetchChaptersError) }, ChaptersController::onFetchChaptersError)
} }
/** /**
@ -308,7 +277,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, _ -> .subscribeFirst({ view, _ ->
view.onChaptersDeleted() view.onChaptersDeleted()
}, ChaptersFragment::onChaptersDeletedError) }, ChaptersController::onChaptersDeletedError)
} }
/** /**
@ -386,7 +355,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
* Adds manga to library * Adds manga to library
*/ */
fun addToLibrary() { fun addToLibrary() {
SharedData.get(MangaFavoriteEvent::class.java)?.call(true) mangaFavoriteRelay.call(true)
} }
/** /**

View File

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

View File

@ -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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.manga.info package eu.kanade.tachiyomi.ui.manga.info
import android.os.Bundle 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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@ -8,52 +10,29 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.Source 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.MangaEvent
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Presenter of MangaInfoFragment. * Presenter of MangaInfoFragment.
* Contains information and data for fragment. * Contains information and data for fragment.
* Observable updates should be called from here. * Observable updates should be called from here.
*/ */
class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() { class MangaInfoPresenter(
val manga: Manga,
/** val source: Source,
* Active manga. private val chapterCountRelay: BehaviorRelay<Int>,
*/ private val mangaFavoriteRelay: PublishRelay<Boolean>,
lateinit var manga: Manga private val db: DatabaseHelper = Injekt.get(),
private set private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
/** ) : BasePresenter<MangaInfoController>() {
* 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. * Subscription to send the manga to the view.
@ -67,24 +46,17 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
source = sourceManager.get(manga.source)!!
sendMangaToView() sendMangaToView()
// Update chapter count // Update chapter count
SharedData.get(ChapterCountEvent::class.java)?.observable chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
?.observeOn(AndroidSchedulers.mainThread()) .subscribeLatestCache(MangaInfoController::setChapterCount)
?.subscribeLatestCache(MangaInfoFragment::setChapterCount)
// Update favorite status // Update favorite status
SharedData.get(MangaFavoriteEvent::class.java)?.let { mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
it.observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { setFavorite(it) } .subscribe { setFavorite(it) }
.apply { add(this) } .apply { add(this) }
} }
}
/** /**
* Sends the active manga to the view. * Sends the active manga to the view.
@ -110,9 +82,9 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { sendMangaToView() } .doOnNext { sendMangaToView() }
.subscribeFirst({ view, manga -> .subscribeFirst({ view, _ ->
view.onFetchMangaDone() view.onFetchMangaDone()
}, { view, error -> }, { view, _ ->
view.onFetchMangaError() view.onFetchMangaError()
}) })
} }
@ -159,7 +131,7 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
* @return List of categories, default plus user categories * @return List of categories, default plus user categories
*/ */
fun getCategories(): List<Category> { fun getCategories(): List<Category> {
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking() return db.getCategories().executeAsBlocking()
} }
/** /**
@ -168,34 +140,30 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
* @param manga the manga to get categories from. * @param manga the manga to get categories from.
* @return Array of category ids the manga is in, if none returns default id * @return Array of category ids the manga is in, if none returns default id
*/ */
fun getMangaCategoryIds(manga: Manga): Array<Int?> { fun getMangaCategoryIds(manga: Manga): Array<Int> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking() val categories = db.getCategoriesForManga(manga).executeAsBlocking()
if(categories.isEmpty()) { return categories.mapNotNull { it.id }.toTypedArray()
return arrayListOf(Category.createDefault().id).toTypedArray()
}
return categories.map { it.id }.toTypedArray()
} }
/** /**
* Move the given manga to categories. * Move the given manga to categories.
* *
* @param categories the selected categories.
* @param manga the manga to move. * @param manga the manga to move.
* @param categories the selected categories.
*/ */
fun moveMangaToCategories(categories: List<Category>, manga: Manga) { fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.map { MangaCategory.create(manga, it) } val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
db.setMangaCategories(mc, arrayListOf(manga))
} }
/** /**
* Move the given manga to the category. * Move the given manga to the category.
* *
* @param category the selected category.
* @param manga the manga to move. * @param manga the manga to move.
* @param category the selected category, or null for default category.
*/ */
fun moveMangaToCategory(category: Category, manga: Manga) { fun moveMangaToCategory(manga: Manga, category: Category?) {
moveMangaToCategories(arrayListOf(category), manga) moveMangaToCategories(manga, listOfNotNull(category))
} }
} }

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() { class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
var items = emptyList<TrackItem>() var items = emptyList<TrackItem>()
set(value) { set(value) {
@ -15,7 +15,11 @@ class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHold
} }
} }
var onClickListener: (TrackItem) -> Unit = {} val rowClickListener: OnRowClickListener = controller
fun getItem(index: Int): TrackItem? {
return items.getOrNull(index)
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
return items.size return items.size
@ -23,11 +27,18 @@ class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHold
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
val view = parent.inflate(R.layout.item_track) val view = parent.inflate(R.layout.item_track)
return TrackHolder(view, fragment) return TrackHolder(view, this)
} }
override fun onBindViewHolder(holder: TrackHolder, position: Int) { override fun onBindViewHolder(holder: TrackHolder, position: Int) {
holder.onSetValues(items[position]) holder.bind(items[position])
}
interface OnRowClickListener {
fun onTitleClick(position: Int)
fun onStatusClick(position: Int)
fun onChaptersClick(position: Int)
fun onScoreClick(position: Int)
} }
} }

View File

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

View File

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

View File

@ -1,25 +1,24 @@
package eu.kanade.tachiyomi.ui.manga.track package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.item_track.view.* import kotlinx.android.synthetic.main.item_track.view.*
class TrackHolder(private val view: View, private val fragment: TrackFragment) class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) {
: RecyclerView.ViewHolder(view) {
private lateinit var item: TrackItem
init { init {
view.title_container.setOnClickListener { fragment.onTitleClick(item) } val listener = adapter.rowClickListener
view.status_container.setOnClickListener { fragment.onStatusClick(item) } view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) } view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
view.score_container.setOnClickListener { fragment.onScoreClick(item) } view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
} }
@SuppressLint("SetTextI18n")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun onSetValues(item: TrackItem) = with(view) { fun bind(item: TrackItem) = with(itemView) {
this@TrackHolder.item = item
val track = item.track val track = item.track
track_logo.setImageResource(item.service.getLogo()) track_logo.setImageResource(item.service.getLogo())
logo.setBackgroundColor(item.service.getLogoColor()) logo.setBackgroundColor(item.service.getLogoColor())

View File

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

View File

@ -4,33 +4,31 @@ import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track 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.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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 eu.kanade.tachiyomi.util.toast
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackPresenter : BasePresenter<TrackFragment>() { class TrackPresenter(
val manga: Manga,
preferences: PreferencesHelper = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(),
private val trackManager: TrackManager = Injekt.get()
) : BasePresenter<TrackController>() {
private val db: DatabaseHelper by injectLazy() private val context = preferences.context
private val trackManager: TrackManager by injectLazy()
lateinit var manga: Manga
private set
private var trackList: List<TrackItem> = emptyList() private var trackList: List<TrackItem> = emptyList()
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
var selectedService: TrackService? = null
private var trackSubscription: Subscription? = null private var trackSubscription: Subscription? = null
private var searchSubscription: Subscription? = null private var searchSubscription: Subscription? = null
@ -39,8 +37,6 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
fetchTrackings() fetchTrackings()
} }
@ -55,7 +51,7 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
} }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { trackList = it } .doOnNext { trackList = it }
.subscribeLatestCache(TrackFragment::onNextTrackings) .subscribeLatestCache(TrackController::onNextTrackings)
} }
fun refresh() { fun refresh() {
@ -72,23 +68,19 @@ class TrackPresenter : BasePresenter<TrackFragment>() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result -> view.onRefreshDone() }, .subscribeFirst({ view, result -> view.onRefreshDone() },
TrackFragment::onRefreshError) TrackController::onRefreshError)
} }
fun search(query: String) { fun search(query: String, service: TrackService) {
val service = selectedService ?: return
searchSubscription?.let { remove(it) } searchSubscription?.let { remove(it) }
searchSubscription = service.search(query) searchSubscription = service.search(query)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(TrackFragment::onSearchResults, .subscribeLatestCache(TrackController::onSearchResults,
TrackFragment::onSearchResultsError) TrackController::onSearchResultsError)
} }
fun registerTracking(item: Track?) { fun registerTracking(item: Track?, service: TrackService) {
val service = selectedService ?: return
if (item != null) { if (item != null) {
item.manga_id = manga.id!! item.manga_id = manga.id!!
add(service.bind(item) add(service.bind(item)

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}
}

View File

@ -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 // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
with(adapter.fragment) { with(adapter.controller) {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_download -> downloadChapter(item) R.id.action_download -> downloadChapter(item)
R.id.action_delete -> deleteChapter(item) R.id.action_delete -> deleteChapter(item)

View File

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

View File

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

View File

@ -1,27 +1,23 @@
package eu.kanade.tachiyomi.ui.recent_updates package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.DividerItemDecoration import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import com.afollestad.materialdialogs.MaterialDialog 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.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog import kotlinx.android.synthetic.main.fragment_recent_chapters.view.*
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_recent_chapters.*
import nucleus.factory.RequiresPresenter
import timber.log.Timber import timber.log.Timber
/** /**
@ -29,22 +25,13 @@ import timber.log.Timber
* Uses [R.layout.fragment_recent_chapters]. * Uses [R.layout.fragment_recent_chapters].
* UI related actions should be called from here. * UI related actions should be called from here.
*/ */
@RequiresPresenter(RecentChaptersPresenter::class) class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
class RecentChaptersFragment: NoToolbarElevationController,
BaseRxFragment<RecentChaptersPresenter>(),
ActionMode.Callback, ActionMode.Callback,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener{ FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.OnUpdateListener,
companion object { ConfirmDeleteChaptersDialog.Listener {
/**
* Create new RecentChaptersFragment.
* @return a new instance of [RecentChaptersFragment].
*/
fun newInstance(): RecentChaptersFragment {
return RecentChaptersFragment()
}
}
/** /**
* Action mode for multiple selection. * Action mode for multiple selection.
@ -54,63 +41,60 @@ class RecentChaptersFragment:
/** /**
* Adapter containing the recent chapters. * Adapter containing the recent chapters.
*/ */
lateinit var adapter: RecentChaptersAdapter var adapter: RecentChaptersAdapter? = null
private set private set
/** override fun getTitle(): String? {
* Called when view gets created return resources?.getString(R.string.label_recent_updates)
* @param inflater layout inflater }
* @param container view group
* @param savedState status of saved state override fun createPresenter(): RecentChaptersPresenter {
*/ return RecentChaptersPresenter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { }
// Inflate view
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.fragment_recent_chapters, container, false) return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
} }
/** /**
* Called when view is created * Called when view is created
* @param view created view * @param view created view
* @param savedState status of saved sate * @param savedViewState status of saved sate
*/ */
override fun onViewCreated(view: View, savedState: Bundle?) { override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
with(view) {
// Init RecyclerView and adapter // Init RecyclerView and adapter
recycler.layoutManager = LinearLayoutManager(activity) val layoutManager = LinearLayoutManager(context)
recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
adapter = RecentChaptersAdapter(this) adapter = RecentChaptersAdapter(this@RecentChaptersController)
recycler.adapter = adapter recycler.adapter = adapter
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { recycler.scrollStateChanges().subscribeUntilDestroy {
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
// Disable swipe refresh when view is not at the top // Disable swipe refresh when view is not at the top
val firstPos = (recycler.layoutManager as LinearLayoutManager) val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
.findFirstCompletelyVisibleItemPosition()
swipe_refresh.isEnabled = firstPos == 0 swipe_refresh.isEnabled = firstPos == 0
} }
})
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.setOnRefreshListener { swipe_refresh.refreshes().subscribeUntilDestroy {
if (!LibraryUpdateService.isRunning(activity)) { if (!LibraryUpdateService.isRunning(context)) {
LibraryUpdateService.start(activity) LibraryUpdateService.start(context)
context.toast(R.string.action_update_library) context.toast(R.string.action_update_library)
} }
// It can be a very long operation, so we disable swipe refresh and show a toast. // It can be a very long operation, so we disable swipe refresh and show a toast.
swipe_refresh.isRefreshing = false 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() { override fun onDestroyView(view: View) {
// Restore toolbar elevation. super.onDestroyView(view)
activity.appbar.enableElevation() adapter = null
super.onDestroyView() actionMode = null
} }
/** /**
@ -118,6 +102,7 @@ class RecentChaptersFragment:
* @return list of selected chapters * @return list of selected chapters
*/ */
fun getSelectedChapters(): List<RecentChapterItem> { fun getSelectedChapters(): List<RecentChapterItem> {
val adapter = adapter ?: return emptyList()
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem } return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
} }
@ -126,6 +111,8 @@ class RecentChaptersFragment:
* @param position position of clicked item * @param position position of clicked item
*/ */
override fun onItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
val adapter = adapter ?: return false
// Get item from position // Get item from position
val item = adapter.getItem(position) as? RecentChapterItem ?: return false val item = adapter.getItem(position) as? RecentChapterItem ?: return false
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
@ -153,14 +140,9 @@ class RecentChaptersFragment:
* @param position position of selected item * @param position position of selected item
*/ */
private fun toggleSelection(position: Int) { private fun toggleSelection(position: Int) {
val adapter = adapter ?: return
adapter.toggleSelection(position) adapter.toggleSelection(position)
actionMode?.invalidate()
val count = adapter.selectedItemCount
if (count == 0) {
actionMode?.finish()
} else {
actionMode?.title = getString(R.string.label_selected, count)
}
} }
/** /**
@ -168,6 +150,7 @@ class RecentChaptersFragment:
* @param chapter selected chapter * @param chapter selected chapter
*/ */
private fun openChapter(item: RecentChapterItem) { private fun openChapter(item: RecentChapterItem) {
val activity = activity ?: return
val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter) val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
startActivity(intent) startActivity(intent)
} }
@ -186,11 +169,17 @@ class RecentChaptersFragment:
* @param chapters list of [Any] * @param chapters list of [Any]
*/ */
fun onNextRecentChapters(chapters: List<IFlexible<*>>) { fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
(activity as MainActivity).updateEmptyView(chapters.isEmpty(),
R.string.information_no_recent, R.drawable.ic_update_black_128dp)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
adapter.updateDataSet(chapters.toMutableList()) 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)
}
} }
/** /**
@ -206,7 +195,7 @@ class RecentChaptersFragment:
* @param download [Download] object containing download progress. * @param download [Download] object containing download progress.
*/ */
private fun getHolder(download: Download): RecentChapterHolder? { private fun getHolder(download: Download): RecentChapterHolder? {
return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
} }
/** /**
@ -220,14 +209,10 @@ class RecentChaptersFragment:
} }
} }
/** override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) {
* Delete selected chapters
* @param chapters list of [RecentChapter] objects
*/
fun deleteChapters(chapters: List<RecentChapterItem>) {
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) DeletingChaptersDialog().showDialog(router)
presenter.deleteChapters(chapters) presenter.deleteChapters(chaptersToDelete)
} }
/** /**
@ -258,7 +243,7 @@ class RecentChaptersFragment:
* @param chapter selected chapter with manga * @param chapter selected chapter with manga
*/ */
fun deleteChapter(chapter: RecentChapterItem) { fun deleteChapter(chapter: RecentChapterItem) {
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG) DeletingChaptersDialog().showDialog(router)
presenter.deleteChapters(listOf(chapter)) presenter.deleteChapters(listOf(chapter))
} }
@ -267,7 +252,7 @@ class RecentChaptersFragment:
*/ */
fun onChaptersDeleted() { fun onChaptersDeleted() {
dismissDeletingDialog() dismissDeletingDialog()
adapter.notifyDataSetChanged() adapter?.notifyDataSetChanged()
} }
/** /**
@ -283,33 +268,7 @@ class RecentChaptersFragment:
* Called to dismiss deleting dialog * Called to dismiss deleting dialog
*/ */
fun dismissDeletingDialog() { fun dismissDeletingDialog() {
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment) router.popControllerWithTag(DeletingChaptersDialog.TAG)
?.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
} }
/** /**
@ -319,21 +278,45 @@ class RecentChaptersFragment:
*/ */
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu) mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
adapter.mode = FlexibleAdapter.MODE_MULTI adapter?.mode = FlexibleAdapter.MODE_MULTI
return true return true
} }
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { 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 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 * Called when ActionMode destroyed
* @param mode the ActionMode object * @param mode the ActionMode object
*/ */
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
adapter.mode = FlexibleAdapter.MODE_IDLE adapter?.mode = FlexibleAdapter.MODE_IDLE
adapter.clearSelection() adapter?.clearSelection()
actionMode = null actionMode = null
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,12 @@
package eu.kanade.tachiyomi.ui.recently_read package eu.kanade.tachiyomi.ui.recently_read
import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.widget.DialogCheckboxView
import kotlinx.android.synthetic.main.item_recently_read.view.* import kotlinx.android.synthetic.main.item_recently_read.view.*
import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.* import java.util.*
/** /**
@ -23,39 +18,47 @@ import java.util.*
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
* @constructor creates a new recent chapter holder. * @constructor creates a new recent chapter holder.
*/ */
class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) class RecentlyReadHolder(
: RecyclerView.ViewHolder(view) { view: View,
val adapter: RecentlyReadAdapter
) : FlexibleViewHolder(view, adapter) {
/** init {
* DecimalFormat used to display correct chapter number itemView.remove.setOnClickListener {
*/ adapter.removeClickListener.onRemoveClick(adapterPosition)
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) }
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 * Set values of view
* *
* @param item item containing history information * @param item item containing history information
*/ */
fun onSetValues(item: MangaChapterHistory) { fun bind(item: MangaChapterHistory) {
// Retrieve objects // Retrieve objects
val manga = item.manga val (manga, chapter, history) = item
val chapter = item.chapter
val history = item.history
// Set manga title // Set manga title
itemView.manga_title.text = manga.title itemView.manga_title.text = manga.title
// Set source + chapter 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) itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source)
.format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber) .format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber)
// Set last read timestamp title // 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 // Set cover
Glide.clear(itemView.cover)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(itemView.context) Glide.with(itemView.context)
.load(manga) .load(manga)
@ -64,40 +67,6 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
.into(itemView.cover) .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)
}
} }
} }

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import android.support.v7.preference.XpPreferenceFragment
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.LocaleHelper import eu.kanade.tachiyomi.util.LocaleHelper

View File

@ -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)
}
}
}

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<RelativeLayout <LinearLayout
android:orientation="vertical"
android:id="@+id/main_content" android:id="@+id/main_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -32,22 +33,12 @@
</eu.kanade.tachiyomi.widget.ElevationAppBarLayout> </eu.kanade.tachiyomi.widget.ElevationAppBarLayout>
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
<FrameLayout android:id="@+id/controller_container"
android:id="@+id/frame_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:layout_below="@id/appbar">
<eu.kanade.tachiyomi.widget.EmptyView </LinearLayout>
android:id="@+id/empty_view"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_height="wrap_content"/>
</FrameLayout>
</RelativeLayout>
<android.support.design.widget.NavigationView <android.support.design.widget.NavigationView
android:id="@+id/nav_view" android:id="@+id/nav_view"
@ -58,4 +49,5 @@
android:theme="?attr/navigation_view_theme" android:theme="?attr/navigation_view_theme"
app:headerLayout="@layout/navigation_header" app:headerLayout="@layout/navigation_header"
app:menu="@menu/menu_navigation"/> app:menu="@menu/menu_navigation"/>
</android.support.v4.widget.DrawerLayout> </android.support.v4.widget.DrawerLayout>

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