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

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

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

View file

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

View file

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

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

@ -1,49 +1,49 @@
package eu.kanade.tachiyomi.ui.library 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.kanade.tachiyomi.data.database.models.Manga import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.android.synthetic.main.item_catalogue_grid.view.* import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
* All the elements from the layout file "item_catalogue_grid" are available in this class. * All the elements from the layout file "item_catalogue_grid" are available in this class.
* *
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
* @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
* holder with the given manga. * holder with the given manga.
* *
* @param manga the manga to bind. * @param manga the manga to bind.
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
// Update the title of the manga. // Update the title of the manga.
view.title.text = manga.title view.title.text = manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(view.unread_text) { with(view.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString() text = manga.unread.toString()
} }
// Update the cover. // Update the cover.
Glide.clear(view.thumbnail) Glide.clear(view.thumbnail)
Glide.with(view.context) Glide.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop() .centerCrop()
.into(view.thumbnail) .into(view.thumbnail)
} }
} }

View file

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

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

@ -1,57 +1,57 @@
package eu.kanade.tachiyomi.ui.library 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.kanade.tachiyomi.data.database.models.Manga import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.android.synthetic.main.item_catalogue_list.view.* import kotlinx.android.synthetic.main.item_catalogue_list.view.*
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
* All the elements from the layout file "item_library_list" are available in this class. * All the elements from the layout file "item_library_list" are available in this class.
* *
* @param view the inflated view for this holder. * @param view the inflated view for this holder.
* @param adapter the adapter handling this holder. * @param adapter the adapter handling this holder.
* @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 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
* holder with the given manga. * holder with the given manga.
* *
* @param manga the manga to bind. * @param manga the manga to bind.
*/ */
override fun onSetValues(manga: Manga) { override fun onSetValues(manga: Manga) {
// Update the title of the manga. // Update the title of the manga.
itemView.title.text = manga.title itemView.title.text = manga.title
// Update the unread count and its visibility. // Update the unread count and its visibility.
with(itemView.unread_text) { with(itemView.unread_text) {
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
text = manga.unread.toString() text = manga.unread.toString()
} }
// Create thumbnail onclick to simulate long click // Create thumbnail onclick to simulate long click
itemView.thumbnail.setOnClickListener { itemView.thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode // Simulate long click on this view to enter selection mode
onLongClick(itemView) onLongClick(itemView)
} }
// Update the cover. // Update the cover.
Glide.clear(itemView.thumbnail) Glide.clear(itemView.thumbnail)
Glide.with(itemView.context) Glide.with(itemView.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop() .centerCrop()
.dontAnimate() .dontAnimate()
.into(itemView.thumbnail) .into(itemView.thumbnail)
} }
} }

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

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

View file

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

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

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

View file

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

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

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

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

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

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

View file

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.manga.track 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

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

View file

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

View file

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

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

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 }
when (manga.sorting) {
Manga.SORTING_SOURCE -> {
chapters.getOrNull(currChapterIndex + 1)
}
Manga.SORTING_NUMBER -> {
val chapterNumber = chapter.chapter_number
var nextChapter: Chapter? = null val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
for (i in (currChapterIndex + 1) until chapters.size) { return when (manga.sorting) {
val c = chapters[i] Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
if (c.chapter_number > chapterNumber && Manga.SORTING_NUMBER -> {
c.chapter_number <= chapterNumber + 1) { val chapterNumber = chapter.chapter_number
nextChapter = c ((currChapterIndex + 1) until chapters.size)
break .map { chapters[it] }
} .firstOrNull { it.chapter_number > chapterNumber &&
} it.chapter_number <= chapterNumber + 1
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>

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