mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 21:11:52 +01:00
UI with Conductor (#784)
This commit is contained in:
parent
89b293fecd
commit
2eeac0bf8b
@ -100,6 +100,16 @@ android {
|
||||
|
||||
dependencies {
|
||||
|
||||
compile "com.bluelinelabs:conductor:2.1.3"
|
||||
|
||||
final rxbindings_version = '1.0.1'
|
||||
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
|
||||
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||
|
||||
// Modified dependencies
|
||||
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
|
||||
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||
@ -212,7 +222,7 @@ dependencies {
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.1.1'
|
||||
ext.kotlin_version = '1.1.2'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -32,10 +32,6 @@
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.manga.MangaActivity"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:theme="@style/Theme.Reader" />
|
||||
@ -43,10 +39,6 @@
|
||||
android:name=".ui.setting.SettingsActivity"
|
||||
android:label="@string/label_settings"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".ui.category.CategoryActivity"
|
||||
android:label="@string/label_categories"
|
||||
android:parentActivityName=".ui.main.MainActivity" />
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
|
@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
*/
|
||||
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
|
@ -1,7 +1,5 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import nucleus.view.NucleusAppCompatActivity
|
||||
@ -14,17 +12,6 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
val superFactory = presenterFactory
|
||||
setPresenterFactory {
|
||||
superFactory.createPresenter().apply {
|
||||
val app = application as App
|
||||
context = app.applicationContext
|
||||
}
|
||||
}
|
||||
super.onCreate(savedState)
|
||||
}
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface NoToolbarElevationController
|
@ -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))
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
|
||||
abstract class BaseFragment : Fragment(), FragmentMixin {
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.support.v4.app.FragmentActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
||||
|
||||
interface FragmentMixin {
|
||||
|
||||
fun setToolbarTitle(title: String) {
|
||||
(getActivity() as ActivityMixin).setToolbarTitle(title)
|
||||
}
|
||||
|
||||
fun setToolbarTitle(resourceId: Int) {
|
||||
(getActivity() as ActivityMixin).setToolbarTitle(getString(resourceId))
|
||||
}
|
||||
|
||||
fun getActivity(): FragmentActivity
|
||||
|
||||
fun getString(resource: Int): String
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import android.content.Context
|
||||
import nucleus.presenter.RxPresenter
|
||||
import nucleus.view.ViewWithPresenter
|
||||
import rx.Observable
|
||||
|
||||
open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
|
||||
|
||||
lateinit var context: Context
|
||||
open class BasePresenter<V> : RxPresenter<V>() {
|
||||
|
||||
/**
|
||||
* Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.ui.catalogue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
@ -19,11 +19,16 @@ class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>()
|
||||
return R.layout.item_catalogue_grid
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): CatalogueHolder {
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): CatalogueHolder {
|
||||
|
||||
if (parent is AutofitRecyclerView) {
|
||||
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
||||
card.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||
card.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(
|
||||
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
|
||||
}
|
||||
return CatalogueGridHolder(view, adapter)
|
||||
} else {
|
||||
@ -32,7 +37,11 @@ class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>()
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: CatalogueHolder, position: Int, payloads: List<Any?>?) {
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: CatalogueHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.onSetValues(manga)
|
||||
}
|
||||
|
||||
|
@ -25,32 +25,18 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [CatalogueFragment].
|
||||
* Presenter of [CatalogueController].
|
||||
*/
|
||||
open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
*/
|
||||
val coverCache: CoverCache by injectLazy()
|
||||
open class CataloguePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val prefs: PreferencesHelper = Injekt.get(),
|
||||
val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<CatalogueController>() {
|
||||
|
||||
/**
|
||||
* Enabled sources.
|
||||
@ -182,7 +168,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ view, page ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, CatalogueFragment::onAddPageError)
|
||||
}, CatalogueController::onAddPageError)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -404,7 +390,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
* @return List of categories, default plus user categories
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -415,10 +401,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (categories.isEmpty()) {
|
||||
return arrayListOf(Category.createDefault().id).toTypedArray()
|
||||
}
|
||||
return categories.map { it.id }.toTypedArray()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -427,10 +410,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
* @param categories the selected categories.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
|
||||
val mc = categories.map { MangaCategory.create(manga, it) }
|
||||
|
||||
db.setMangaCategories(mc, arrayListOf(manga))
|
||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -439,8 +421,8 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
* @param category the selected category.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategory(category: Category, manga: Manga) {
|
||||
moveMangaToCategories(arrayListOf(category), manga)
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -454,7 +436,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
if (!manga.favorite)
|
||||
changeMangaFavorite(manga)
|
||||
|
||||
moveMangaToCategories(selectedCategories.filter { it.id != 0 }, manga)
|
||||
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
|
||||
} else {
|
||||
changeMangaFavorite(manga)
|
||||
}
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
}
|
@ -3,31 +3,48 @@ package eu.kanade.tachiyomi.ui.category
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
/**
|
||||
* Adapter of CategoryHolder.
|
||||
* Connection between Activity and Holder
|
||||
* Holder updates should be called from here.
|
||||
* Custom adapter for categories.
|
||||
*
|
||||
* @param activity activity that created adapter
|
||||
* @constructor Creates a CategoryAdapter object
|
||||
* @param controller The containing controller.
|
||||
*/
|
||||
class CategoryAdapter(private val activity: CategoryActivity) :
|
||||
FlexibleAdapter<CategoryItem>(null, activity, true) {
|
||||
class CategoryAdapter(controller: CategoryController) :
|
||||
FlexibleAdapter<CategoryItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Called when item is released.
|
||||
* Listener called when an item of the list is released.
|
||||
*/
|
||||
fun onItemReleased() {
|
||||
activity.onItemReleased()
|
||||
}
|
||||
val onItemReleaseListener: OnItemReleaseListener = controller
|
||||
|
||||
/**
|
||||
* Clears the active selections from the list and the model.
|
||||
*/
|
||||
override fun clearSelection() {
|
||||
super.clearSelection()
|
||||
(0..itemCount-1).forEach { getItem(it).isSelected = false }
|
||||
(0 until itemCount).forEach { getItem(it).isSelected = false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the active selections from the model.
|
||||
*/
|
||||
fun clearModelSelection() {
|
||||
selectedPositions.forEach { getItem(it).isSelected = false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection of the given position.
|
||||
*
|
||||
* @param position The position to toggle.
|
||||
*/
|
||||
override fun toggleSelection(position: Int) {
|
||||
super.toggleSelection(position)
|
||||
getItem(position).isSelected = isSelected(position)
|
||||
}
|
||||
|
||||
interface OnItemReleaseListener {
|
||||
/**
|
||||
* Called when an item of the list is released.
|
||||
*/
|
||||
fun onItemReleased(position: Int)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -10,14 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import kotlinx.android.synthetic.main.item_edit_categories.view.*
|
||||
|
||||
/**
|
||||
* Holder that contains category item.
|
||||
* Uses R.layout.item_edit_categories.
|
||||
* UI related actions should be called from here.
|
||||
* Holder used to display category items.
|
||||
*
|
||||
* @param view view of category item.
|
||||
* @param adapter adapter belonging to holder.
|
||||
*
|
||||
* @constructor Create CategoryHolder object
|
||||
* @param view The view used by category items.
|
||||
* @param adapter The adapter containing this holder.
|
||||
*/
|
||||
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
@ -32,9 +28,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
||||
}
|
||||
|
||||
/**
|
||||
* Update category item values.
|
||||
* Binds this holder with the given category.
|
||||
*
|
||||
* @param category category of item.
|
||||
* @param category The category to bind.
|
||||
*/
|
||||
fun bind(category: Category) {
|
||||
// Set capitalized title.
|
||||
@ -47,9 +43,9 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns circle letter image
|
||||
* Returns circle letter image.
|
||||
*
|
||||
* @param text first letter of string
|
||||
* @param text The first letter of string.
|
||||
*/
|
||||
private fun getRound(text: String): TextDrawable {
|
||||
val size = Math.min(itemView.image.width, itemView.image.height)
|
||||
@ -63,9 +59,14 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
|
||||
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is released.
|
||||
*
|
||||
* @param position The position of the released item.
|
||||
*/
|
||||
override fun onItemReleased(position: Int) {
|
||||
super.onItemReleased(position)
|
||||
adapter.onItemReleased()
|
||||
adapter.onItemReleaseListener.onItemReleased(position)
|
||||
}
|
||||
|
||||
}
|
@ -8,29 +8,62 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
/**
|
||||
* Category item for a recycler view.
|
||||
*/
|
||||
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
|
||||
|
||||
/**
|
||||
* Whether this item is currently selected.
|
||||
*/
|
||||
var isSelected = false
|
||||
|
||||
/**
|
||||
* Returns the layout resource for this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.item_edit_categories
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
|
||||
/**
|
||||
* Returns a new view holder for this item.
|
||||
*
|
||||
* @param adapter The adapter of this item.
|
||||
* @param inflater The layout inflater for XML inflation.
|
||||
* @param parent The container view.
|
||||
*/
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): CategoryHolder {
|
||||
|
||||
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CategoryHolder,
|
||||
position: Int, payloads: List<Any?>?) {
|
||||
/**
|
||||
* Binds the given view holder with this item.
|
||||
*
|
||||
* @param adapter The adapter of this item.
|
||||
* @param holder The holder to bind.
|
||||
* @param position The position of this item in the adapter.
|
||||
* @param payloads List of partial changes.
|
||||
*/
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: CategoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this item is draggable.
|
||||
*/
|
||||
override fun isDraggable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is CategoryItem) {
|
||||
return category.id == other.category.id
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of CategoryActivity.
|
||||
* Contains information and data for activity.
|
||||
* Observable updates should be called from here.
|
||||
* Presenter of [CategoryController]. Used to manage the categories of the library.
|
||||
*/
|
||||
class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
||||
|
||||
/**
|
||||
* Used to connect to database.
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
class CategoryPresenter(
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
) : BasePresenter<CategoryController>() {
|
||||
|
||||
/**
|
||||
* List containing categories.
|
||||
*/
|
||||
private var categories: List<Category> = emptyList()
|
||||
|
||||
/**
|
||||
* Called when the presenter is created.
|
||||
*
|
||||
* @param savedState The saved state of this presenter.
|
||||
*/
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@ -33,18 +33,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
||||
.doOnNext { categories = it }
|
||||
.map { it.map(::CategoryItem) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(CategoryActivity::setCategories)
|
||||
.subscribeLatestCache(CategoryController::setCategories)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create category and add it to database
|
||||
* Creates and adds a new category to the database.
|
||||
*
|
||||
* @param name name of category
|
||||
* @param name The name of the category to create.
|
||||
*/
|
||||
fun createCategory(name: String) {
|
||||
// Do not allow duplicate categories.
|
||||
if (categories.any { it.name.equals(name, true) }) {
|
||||
context.toast(R.string.error_category_exists)
|
||||
if (categoryExists(name)) {
|
||||
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||
return
|
||||
}
|
||||
|
||||
@ -59,18 +59,18 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete category from database
|
||||
* Deletes the given categories from the database.
|
||||
*
|
||||
* @param categories list of categories
|
||||
* @param categories The list of categories to delete.
|
||||
*/
|
||||
fun deleteCategories(categories: List<Category>) {
|
||||
db.deleteCategories(categories).asRxObservable().subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder categories in database
|
||||
* Reorders the given categories in the database.
|
||||
*
|
||||
* @param categories list of categories
|
||||
* @param categories The list of categories to reorder.
|
||||
*/
|
||||
fun reorderCategories(categories: List<Category>) {
|
||||
categories.forEachIndexed { i, category ->
|
||||
@ -81,19 +81,27 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a category
|
||||
* Renames a category.
|
||||
*
|
||||
* @param category category that gets renamed
|
||||
* @param name new name of category
|
||||
* @param category The category to rename.
|
||||
* @param name The new name of the category.
|
||||
*/
|
||||
fun renameCategory(category: Category, name: String) {
|
||||
// Do not allow duplicate categories.
|
||||
if (categories.any { it.name.equals(name, true) }) {
|
||||
context.toast(R.string.error_category_exists)
|
||||
if (categoryExists(name)) {
|
||||
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||
return
|
||||
}
|
||||
|
||||
category.name = name
|
||||
db.insertCategory(category).asRxObservable().subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a category with the given name already exists.
|
||||
*/
|
||||
fun categoryExists(name: String): Boolean {
|
||||
return categories.any { it.name.equals(name, true) }
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_download_queue.*
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
@ -242,6 +241,6 @@ class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
|
||||
}
|
||||
|
||||
fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
|
||||
if (show) empty_view.show(drawable, textResource) else empty_view.hide()
|
||||
// if (show) empty_view.show(drawable, textResource) else empty_view.hide()
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
|
||||
/**
|
||||
* Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
|
||||
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
||||
|
||||
|
@ -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>)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,88 +1,88 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
|
||||
/**
|
||||
* This adapter stores the categories from the library, used with a ViewPager.
|
||||
*
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class LibraryAdapter(private val fragment: LibraryFragment) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
val view = container.inflate(R.layout.item_library_category) as LibraryCategoryView
|
||||
view.onCreate(fragment)
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a view with a position.
|
||||
*
|
||||
* @param view the view to bind.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun bindView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onBind(categories[position])
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycles a view.
|
||||
*
|
||||
* @param view the view to recycle.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onRecycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of categories.
|
||||
*
|
||||
* @return the number of categories or 0 if the list is null.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return categories.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display for a category.
|
||||
*
|
||||
* @param position the position of the element.
|
||||
* @return the title to display.
|
||||
*/
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return categories[position].name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the view.
|
||||
*/
|
||||
override fun getItemPosition(obj: Any?): Int {
|
||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||
return if (index == -1) POSITION_NONE else index
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
|
||||
/**
|
||||
* This adapter stores the categories from the library, used with a ViewPager.
|
||||
*
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* The categories to bind in the adapter.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
// This setter helps to not refresh the adapter if the reference to the list doesn't change.
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
val view = container.inflate(R.layout.item_library_category2) as LibraryCategoryView
|
||||
view.onCreate(controller)
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a view with a position.
|
||||
*
|
||||
* @param view the view to bind.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun bindView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onBind(categories[position])
|
||||
}
|
||||
|
||||
/**
|
||||
* Recycles a view.
|
||||
*
|
||||
* @param view the view to recycle.
|
||||
* @param position the position in the adapter.
|
||||
*/
|
||||
override fun recycleView(view: View, position: Int) {
|
||||
(view as LibraryCategoryView).onRecycle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of categories.
|
||||
*
|
||||
* @return the number of categories or 0 if the list is null.
|
||||
*/
|
||||
override fun getCount(): Int {
|
||||
return categories.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title to display for a category.
|
||||
*
|
||||
* @param position the position of the element.
|
||||
* @return the title to display.
|
||||
*/
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return categories[position].name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the view.
|
||||
*/
|
||||
override fun getItemPosition(obj: Any?): Int {
|
||||
val view = obj as? LibraryCategoryView ?: return POSITION_NONE
|
||||
val index = categories.indexOfFirst { it.id == view.category.id }
|
||||
return if (index == -1) POSITION_NONE else index
|
||||
}
|
||||
|
||||
}
|
@ -1,122 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
*
|
||||
* @param fragment the fragment containing this adapter.
|
||||
*/
|
||||
class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
||||
FlexibleAdapter<LibraryHolder, Manga>() {
|
||||
|
||||
/**
|
||||
* The list of manga in this category.
|
||||
*/
|
||||
private var mangas: List<Manga> = emptyList()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a list of manga in the adapter.
|
||||
*
|
||||
* @param list the list to set.
|
||||
*/
|
||||
fun setItems(list: List<Manga>) {
|
||||
mItems = list
|
||||
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = ArrayList(list)
|
||||
updateDataSet(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier for a manga.
|
||||
*
|
||||
* @param position the position in the adapter.
|
||||
* @return an identifier for the item.
|
||||
*/
|
||||
override fun getItemId(position: Int): Long {
|
||||
return mItems[position].id!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the list of manga applying [filterObject] for each element.
|
||||
*
|
||||
* @param param the filter. Not used.
|
||||
*/
|
||||
override fun updateDataSet(param: String?) {
|
||||
filterItems(mangas)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a manga depending on a query.
|
||||
*
|
||||
* @param manga the manga to filter.
|
||||
* @param query the query to apply.
|
||||
* @return true if the manga should be included, false otherwise.
|
||||
*/
|
||||
override fun filterObject(manga: Manga, query: String): Boolean = with(manga) {
|
||||
title.toLowerCase().contains(query) ||
|
||||
author != null && author!!.toLowerCase().contains(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new view holder.
|
||||
*
|
||||
* @param parent the parent view.
|
||||
* @param viewType the type of the holder.
|
||||
* @return a new view holder for a manga.
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryHolder {
|
||||
// Depending on preferences, display a list or display a grid
|
||||
if (parent is AutofitRecyclerView) {
|
||||
val view = parent.inflate(R.layout.item_catalogue_grid).apply {
|
||||
val coverHeight = parent.itemWidth / 3 * 4
|
||||
card.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight)
|
||||
gradient.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, coverHeight / 2, Gravity.BOTTOM)
|
||||
}
|
||||
return LibraryGridHolder(view, this, fragment)
|
||||
} else {
|
||||
val view = parent.inflate(R.layout.item_catalogue_list)
|
||||
return LibraryListHolder(view, this, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a holder with a new position.
|
||||
*
|
||||
* @param holder the holder to bind.
|
||||
* @param position the position to bind.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: LibraryHolder, position: Int) {
|
||||
val manga = getItem(position)
|
||||
|
||||
holder.onSetValues(manga)
|
||||
// When user scrolls this bind the correct selection status
|
||||
holder.itemView.isActivated = isSelected(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position in the adapter for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return mangas.orEmpty().indexOfFirst { it.id == manga.id }
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* Adapter storing a list of manga in a certain category.
|
||||
*
|
||||
* @param view the fragment containing this adapter.
|
||||
*/
|
||||
class LibraryCategoryAdapter(view: LibraryCategoryView) :
|
||||
FlexibleAdapter<LibraryItem>(null, view, true) {
|
||||
|
||||
/**
|
||||
* The list of manga in this category.
|
||||
*/
|
||||
private var mangas: List<LibraryItem> = emptyList()
|
||||
|
||||
/**
|
||||
* Sets a list of manga in the adapter.
|
||||
*
|
||||
* @param list the list to set.
|
||||
*/
|
||||
fun setItems(list: List<LibraryItem>) {
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = list.toList()
|
||||
|
||||
performFilter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position in the adapter for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
*/
|
||||
fun indexOf(manga: Manga): Int {
|
||||
return mangas.indexOfFirst { it.manga.id == manga.id }
|
||||
}
|
||||
|
||||
fun performFilter() {
|
||||
updateDataSet(mangas.filter { it.filter(searchText) })
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,266 +1,248 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.item_library_category.view.*
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Fragment containing the library manga for a certain category.
|
||||
* Uses R.layout.fragment_library_category.
|
||||
*/
|
||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||
: FrameLayout(context, attrs), FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
private lateinit var fragment: LibraryFragment
|
||||
|
||||
/**
|
||||
* Category for this view.
|
||||
*/
|
||||
lateinit var category: Category
|
||||
private set
|
||||
|
||||
/**
|
||||
* Recycler view of the list of manga.
|
||||
*/
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
/**
|
||||
* Adapter to hold the manga in this category.
|
||||
*/
|
||||
private lateinit var adapter: LibraryCategoryAdapter
|
||||
|
||||
/**
|
||||
* Subscription for the library manga.
|
||||
*/
|
||||
private var libraryMangaSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription of the library search.
|
||||
*/
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription of the library selections.
|
||||
*/
|
||||
private var selectionSubscription: Subscription? = null
|
||||
|
||||
fun onCreate(fragment: LibraryFragment) {
|
||||
this.fragment = fragment
|
||||
|
||||
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
} else {
|
||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = fragment.mangaPerRow
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.addView(recycler)
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos == 0
|
||||
}
|
||||
})
|
||||
|
||||
// Double the distance required to trigger sync
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(context)) {
|
||||
LibraryUpdateService.start(context, category)
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onBind(category: Category) {
|
||||
this.category = category
|
||||
|
||||
val presenter = fragment.presenter
|
||||
|
||||
searchSubscription = presenter.searchSubject.subscribe { text ->
|
||||
adapter.searchText = text
|
||||
adapter.updateDataSet()
|
||||
}
|
||||
|
||||
adapter.mode = if (presenter.selectedMangas.isNotEmpty()) {
|
||||
FlexibleAdapter.MODE_MULTI
|
||||
} else {
|
||||
FlexibleAdapter.MODE_SINGLE
|
||||
}
|
||||
|
||||
libraryMangaSubscription = presenter.libraryMangaSubject
|
||||
.subscribe { onNextLibraryManga(it) }
|
||||
|
||||
selectionSubscription = presenter.selectionSubject
|
||||
.subscribe { onSelectionChanged(it) }
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
adapter.setItems(emptyList())
|
||||
adapter.clearSelection()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
searchSubscription?.unsubscribe()
|
||||
libraryMangaSubscription?.unsubscribe()
|
||||
selectionSubscription?.unsubscribe()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
|
||||
* adapter.
|
||||
*
|
||||
* @param event the event received.
|
||||
*/
|
||||
fun onNextLibraryManga(event: LibraryMangaEvent) {
|
||||
// Get the manga list for this category.
|
||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||
|
||||
// Update the category with its manga.
|
||||
adapter.setItems(mangaForCategory)
|
||||
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
fragment.presenter.selectedMangas.forEach { manga ->
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
|
||||
* depending on the type of event received.
|
||||
*
|
||||
* @param event the selection event received.
|
||||
*/
|
||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
||||
when (event) {
|
||||
is LibrarySelectionEvent.Selected -> {
|
||||
if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
|
||||
adapter.mode = FlexibleAdapter.MODE_MULTI
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
}
|
||||
is LibrarySelectionEvent.Unselected -> {
|
||||
findAndToggleSelection(event.manga)
|
||||
if (fragment.presenter.selectedMangas.isEmpty()) {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
}
|
||||
}
|
||||
is LibrarySelectionEvent.Cleared -> {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
adapter.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection for the given manga and updates the view if needed.
|
||||
*
|
||||
* @param manga the manga to toggle.
|
||||
*/
|
||||
private fun findAndToggleSelection(manga: Manga) {
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onListItemClick(position: Int): Boolean {
|
||||
// If the action mode is created and the position is valid, toggle the selection.
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openManga(item)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onListItemLongClick(position: Int) {
|
||||
fragment.createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a manga.
|
||||
*
|
||||
* @param manga the manga to open.
|
||||
*/
|
||||
private fun openManga(manga: Manga) {
|
||||
// Notify the presenter a manga is being opened.
|
||||
fragment.presenter.onOpenManga()
|
||||
|
||||
// Create a new activity with the manga.
|
||||
val intent = MangaActivity.newIntent(context, manga)
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tells the presenter to toggle the selection for the given position.
|
||||
*
|
||||
* @param position the position to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val manga = adapter.getItem(position) ?: return
|
||||
|
||||
fragment.presenter.setSelection(manga, !adapter.isSelected(position))
|
||||
fragment.invalidateActionMode()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.item_library_category.view.*
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Fragment containing the library manga for a certain category.
|
||||
* Uses R.layout.fragment_library_category.
|
||||
*/
|
||||
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
FrameLayout(context, attrs),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener {
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
private lateinit var controller: LibraryController
|
||||
|
||||
/**
|
||||
* Category for this view.
|
||||
*/
|
||||
lateinit var category: Category
|
||||
private set
|
||||
|
||||
/**
|
||||
* Recycler view of the list of manga.
|
||||
*/
|
||||
private lateinit var recycler: RecyclerView
|
||||
|
||||
/**
|
||||
* Adapter to hold the manga in this category.
|
||||
*/
|
||||
private lateinit var adapter: LibraryCategoryAdapter
|
||||
|
||||
/**
|
||||
* Subscriptions while the view is bound.
|
||||
*/
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
fun onCreate(controller: LibraryController) {
|
||||
this.controller = controller
|
||||
|
||||
recycler = if (preferences.libraryAsList().getOrDefault()) {
|
||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
} else {
|
||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = controller.mangaPerRow
|
||||
}
|
||||
}
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.addView(recycler)
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos == 0
|
||||
}
|
||||
})
|
||||
|
||||
// Double the distance required to trigger sync
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(context)) {
|
||||
LibraryUpdateService.start(context, category)
|
||||
context.toast(R.string.updating_category)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onBind(category: Category) {
|
||||
this.category = category
|
||||
|
||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||
FlexibleAdapter.MODE_MULTI
|
||||
} else {
|
||||
FlexibleAdapter.MODE_SINGLE
|
||||
}
|
||||
|
||||
subscriptions += controller.searchRelay
|
||||
.doOnNext { adapter.searchText = it }
|
||||
.skip(1)
|
||||
.subscribe { adapter.performFilter() }
|
||||
|
||||
subscriptions += controller.libraryMangaRelay
|
||||
.subscribe { onNextLibraryManga(it) }
|
||||
|
||||
subscriptions += controller.selectionRelay
|
||||
.subscribe { onSelectionChanged(it) }
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
adapter.setItems(emptyList())
|
||||
adapter.clearSelection()
|
||||
subscriptions.clear()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
subscriptions.clear()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibraryMangaEvent]. When an event is received, it updates the content of the
|
||||
* adapter.
|
||||
*
|
||||
* @param event the event received.
|
||||
*/
|
||||
fun onNextLibraryManga(event: LibraryMangaEvent) {
|
||||
// Get the manga list for this category.
|
||||
val mangaForCategory = event.getMangaForCategory(category).orEmpty()
|
||||
|
||||
// Update the category with its manga.
|
||||
adapter.setItems(mangaForCategory)
|
||||
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
controller.selectedMangas.forEach { manga ->
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to [LibrarySelectionEvent]. When an event is received, it updates the selection
|
||||
* depending on the type of event received.
|
||||
*
|
||||
* @param event the selection event received.
|
||||
*/
|
||||
private fun onSelectionChanged(event: LibrarySelectionEvent) {
|
||||
when (event) {
|
||||
is LibrarySelectionEvent.Selected -> {
|
||||
if (adapter.mode != FlexibleAdapter.MODE_MULTI) {
|
||||
adapter.mode = FlexibleAdapter.MODE_MULTI
|
||||
}
|
||||
findAndToggleSelection(event.manga)
|
||||
}
|
||||
is LibrarySelectionEvent.Unselected -> {
|
||||
findAndToggleSelection(event.manga)
|
||||
if (controller.selectedMangas.isEmpty()) {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
}
|
||||
}
|
||||
is LibrarySelectionEvent.Cleared -> {
|
||||
adapter.mode = FlexibleAdapter.MODE_SINGLE
|
||||
adapter.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection for the given manga and updates the view if needed.
|
||||
*
|
||||
* @param manga the manga to toggle.
|
||||
*/
|
||||
private fun findAndToggleSelection(manga: Manga) {
|
||||
val position = adapter.indexOf(manga)
|
||||
if (position != -1) {
|
||||
adapter.toggleSelection(position)
|
||||
(recycler.findViewHolderForItemId(manga.id!!) as? LibraryHolder)?.toggleActivation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
// If the action mode is created and the position is valid, toggle the selection.
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openManga(item.manga)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
controller.createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a manga.
|
||||
*
|
||||
* @param manga the manga to open.
|
||||
*/
|
||||
private fun openManga(manga: Manga) {
|
||||
controller.openManga(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the presenter to toggle the selection for the given position.
|
||||
*
|
||||
* @param position the position to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val item = adapter.getItem(position) ?: return
|
||||
|
||||
controller.setSelection(item.manga, !adapter.isSelected(position))
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -1,49 +1,49 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
class LibraryGridHolder(private val view: View,
|
||||
private val adapter: LibraryCategoryAdapter,
|
||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||
: LibraryHolder(view, adapter, listener) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
// Update the title of the manga.
|
||||
view.title.text = manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(view.unread_text) {
|
||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = manga.unread.toString()
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(view.thumbnail)
|
||||
Glide.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.into(view.thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
class LibraryGridHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
// Update the title of the manga.
|
||||
view.title.text = manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(view.unread_text) {
|
||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = manga.unread.toString()
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(view.thumbnail)
|
||||
Glide.with(view.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.into(view.thumbnail)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,27 +1,28 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
|
||||
/**
|
||||
* Generic class used to hold the displayed data of a manga in the library.
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to the single tap and long tap events.
|
||||
*/
|
||||
|
||||
abstract class LibraryHolder(private val view: View,
|
||||
adapter: LibraryCategoryAdapter,
|
||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||
: FlexibleViewHolder(view, adapter, listener) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
abstract fun onSetValues(manga: Manga)
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
/**
|
||||
* Generic class used to hold the displayed data of a manga in the library.
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to the single tap and long tap events.
|
||||
*/
|
||||
|
||||
abstract class LibraryHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<*>
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
abstract fun onSetValues(manga: Manga)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,57 +1,57 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_library_list" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
|
||||
class LibraryListHolder(private val view: View,
|
||||
private val adapter: LibraryCategoryAdapter,
|
||||
listener: FlexibleViewHolder.OnListItemClickListener)
|
||||
: LibraryHolder(view, adapter, listener) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
// Update the title of the manga.
|
||||
itemView.title.text = manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(itemView.unread_text) {
|
||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = manga.unread.toString()
|
||||
}
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
itemView.thumbnail.setOnClickListener {
|
||||
// Simulate long click on this view to enter selection mode
|
||||
onLongClick(itemView)
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(itemView.thumbnail)
|
||||
Glide.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.dontAnimate()
|
||||
.into(itemView.thumbnail)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
* All the elements from the layout file "item_library_list" are available in this class.
|
||||
*
|
||||
* @param view the inflated view for this holder.
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @param listener a listener to react to single tap and long tap events.
|
||||
* @constructor creates a new library holder.
|
||||
*/
|
||||
|
||||
class LibraryListHolder(
|
||||
private val view: View,
|
||||
private val adapter: FlexibleAdapter<*>
|
||||
) : LibraryHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
|
||||
* holder with the given manga.
|
||||
*
|
||||
* @param manga the manga to bind.
|
||||
*/
|
||||
override fun onSetValues(manga: Manga) {
|
||||
// Update the title of the manga.
|
||||
itemView.title.text = manga.title
|
||||
|
||||
// Update the unread count and its visibility.
|
||||
with(itemView.unread_text) {
|
||||
visibility = if (manga.unread > 0) View.VISIBLE else View.GONE
|
||||
text = manga.unread.toString()
|
||||
}
|
||||
|
||||
// Create thumbnail onclick to simulate long click
|
||||
itemView.thumbnail.setOnClickListener {
|
||||
// Simulate long click on this view to enter selection mode
|
||||
onLongClick(itemView)
|
||||
}
|
||||
|
||||
// Update the cover.
|
||||
Glide.clear(itemView.thumbnail)
|
||||
Glide.with(itemView.context)
|
||||
.load(manga)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
||||
.centerCrop()
|
||||
.dontAnimate()
|
||||
.into(itemView.thumbnail)
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class LibraryMangaEvent(val mangas: Map<Int, List<Manga>>) {
|
||||
class LibraryMangaEvent(val mangas: Map<Int, List<LibraryItem>>) {
|
||||
|
||||
fun getMangaForCategory(category: Category): List<Manga>? {
|
||||
fun getMangaForCategory(category: Category): List<LibraryItem>? {
|
||||
return mangas[category.id]
|
||||
}
|
||||
}
|
||||
|
@ -1,373 +1,315 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Pair
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of [LibraryFragment].
|
||||
*/
|
||||
class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
*/
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
|
||||
/**
|
||||
* Currently selected manga.
|
||||
*/
|
||||
val selectedMangas = mutableListOf<Manga>()
|
||||
|
||||
/**
|
||||
* Search query of the library.
|
||||
*/
|
||||
val searchSubject: BehaviorRelay<String> = BehaviorRelay.create()
|
||||
|
||||
/**
|
||||
* Subject to notify the library's viewpager for updates.
|
||||
*/
|
||||
val libraryMangaSubject: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||
|
||||
/**
|
||||
* Subject to notify the UI of selection updates.
|
||||
*/
|
||||
val selectionSubject: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Library subscription.
|
||||
*/
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, tick -> Pair(lib.first, applyFilters(lib.second)) })
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, tick -> Pair(lib.first, applySort(lib.second)) })
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, pair ->
|
||||
view.onNextLibraryUpdate(pair.first, pair.second)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
// Cached list of downloaded manga directories given a source id.
|
||||
val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
|
||||
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Boolean>()
|
||||
|
||||
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
|
||||
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
val filterFn: (Manga) -> Boolean = f@ { manga ->
|
||||
// Filter out manga without source.
|
||||
val source = sourceManager.get(manga.source) ?: return@f false
|
||||
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && manga.unread == 0) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
// Filter when the download directory doesn't exist or is null.
|
||||
if (filterDownloaded) {
|
||||
// Get the directories for the source of the manga.
|
||||
val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
|
||||
val sourceDir = downloadManager.findSourceDir(source)
|
||||
sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
|
||||
}
|
||||
|
||||
val mangaDirName = downloadManager.getMangaDirName(manga)
|
||||
val mangaDir = dirsForSource[mangaDirName] ?: return@f false
|
||||
|
||||
val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
|
||||
mangaDir.listFiles()?.isNotEmpty() ?: false
|
||||
}
|
||||
if (!hasDirs) {
|
||||
return@f false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||
|
||||
val lastReadManga by lazy {
|
||||
var counter = 0
|
||||
db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
|
||||
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
|
||||
val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
|
||||
manga1LastRead.compareTo(manga2LastRead)
|
||||
}
|
||||
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
||||
LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
|
||||
else -> throw Exception("Unknown sorting mode")
|
||||
}
|
||||
}
|
||||
|
||||
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||
Comparator(sortFn)
|
||||
else
|
||||
Collections.reverseOrder(sortFn)
|
||||
|
||||
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
{ dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0))
|
||||
arrayListOf(Category.createDefault()) + dbCategories
|
||||
else
|
||||
dbCategories
|
||||
|
||||
this.categories = categories
|
||||
Pair(categories, libraryManga)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories from the database.
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return db.getCategories().asRxObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manga grouped by categories.
|
||||
*
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
||||
return db.getLibraryMangas().asRxObservable()
|
||||
.map { list -> list.groupBy { it.category } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is opened.
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.let { remove(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection for a given manga.
|
||||
*
|
||||
* @param manga the manga whose selection has changed.
|
||||
* @param selected whether it's now selected or not.
|
||||
*/
|
||||
fun setSelection(manga: Manga, selected: Boolean) {
|
||||
if (selected) {
|
||||
selectedMangas.add(manga)
|
||||
selectionSubject.call(LibrarySelectionEvent.Selected(manga))
|
||||
} else {
|
||||
selectedMangas.remove(manga)
|
||||
selectionSubject.call(LibrarySelectionEvent.Unselected(manga))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the manga selections and notifies the UI.
|
||||
*/
|
||||
fun clearSelections() {
|
||||
selectedMangas.clear()
|
||||
selectionSubject.call(LibrarySelectionEvent.Cleared())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
*
|
||||
* @param deleteChapters whether to also delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangaFromLibrary(deleteChapters: Boolean) {
|
||||
// Create a set of the list
|
||||
val mangaToDelete = selectedMangas.distinctBy { it.id }
|
||||
mangaToDelete.forEach { it.favorite = false }
|
||||
|
||||
Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
|
||||
.onErrorResumeNext { Observable.empty() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
Observable.fromCallable {
|
||||
mangaToDelete.forEach { manga ->
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
if (deleteChapters) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
downloadManager.findMangaDir(source, manga)?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
|
||||
val mc = ArrayList<MangaCategory>()
|
||||
|
||||
for (manga in mangas) {
|
||||
for (cat in categories) {
|
||||
mc.add(MangaCategory.create(manga, cat))
|
||||
}
|
||||
}
|
||||
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file.
|
||||
*
|
||||
* @param inputStream the new cover.
|
||||
* @param manga the manga edited.
|
||||
* @return true if the cover is updated, false otherwise
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
LocalSource.updateCover(context, manga, inputStream)
|
||||
return true
|
||||
}
|
||||
|
||||
if (manga.thumbnail_url != null && manga.favorite) {
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Pair
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of [LibraryController].
|
||||
*/
|
||||
class LibraryPresenter(
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<LibraryController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
var categories: List<Category> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Library subscription.
|
||||
*/
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> Pair(lib.first, applyFilters(lib.second)) })
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, _ -> Pair(lib.first, applySort(lib.second)) })
|
||||
.map { Pair(it.first, it.second.mapValues { it.value.map(::LibraryItem) }) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, pair ->
|
||||
view.onNextLibraryUpdate(pair.first, pair.second)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
// Cached list of downloaded manga directories given a source id.
|
||||
val mangaDirsForSource = mutableMapOf<Long, Map<String?, UniFile>>()
|
||||
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Boolean>()
|
||||
|
||||
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
|
||||
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
val filterFn: (Manga) -> Boolean = f@ { manga ->
|
||||
// Filter out manga without source.
|
||||
val source = sourceManager.get(manga.source) ?: return@f false
|
||||
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && manga.unread == 0) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
// Filter when the download directory doesn't exist or is null.
|
||||
if (filterDownloaded) {
|
||||
// Get the directories for the source of the manga.
|
||||
val dirsForSource = mangaDirsForSource.getOrPut(source.id) {
|
||||
val sourceDir = downloadManager.findSourceDir(source)
|
||||
sourceDir?.listFiles()?.associateBy { it.name }.orEmpty()
|
||||
}
|
||||
|
||||
val mangaDirName = downloadManager.getMangaDirName(manga)
|
||||
val mangaDir = dirsForSource[mangaDirName] ?: return@f false
|
||||
|
||||
val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
|
||||
mangaDir.listFiles()?.isNotEmpty() ?: false
|
||||
}
|
||||
if (!hasDirs) {
|
||||
return@f false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||
|
||||
val lastReadManga by lazy {
|
||||
var counter = 0
|
||||
db.getLastReadManga().executeAsBlocking().associate { it.id!! to counter++ }
|
||||
}
|
||||
|
||||
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[manga1.id!!] ?: lastReadManga.size
|
||||
val manga2LastRead = lastReadManga[manga2.id!!] ?: lastReadManga.size
|
||||
manga1LastRead.compareTo(manga2LastRead)
|
||||
}
|
||||
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
||||
LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
|
||||
else -> throw Exception("Unknown sorting mode")
|
||||
}
|
||||
}
|
||||
|
||||
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||
Comparator(sortFn)
|
||||
else
|
||||
Collections.reverseOrder(sortFn)
|
||||
|
||||
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
{ dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0))
|
||||
arrayListOf(Category.createDefault()) + dbCategories
|
||||
else
|
||||
dbCategories
|
||||
|
||||
this.categories = categories
|
||||
Pair(categories, libraryManga)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories from the database.
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return db.getCategories().asRxObservable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manga grouped by categories.
|
||||
*
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
||||
return db.getLibraryMangas().asRxObservable()
|
||||
.map { list -> list.groupBy { it.category } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is opened.
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
librarySubscription?.let { remove(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the common categories for the given list of manga.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
|
||||
if (mangas.isEmpty()) return emptyList()
|
||||
return mangas.toSet()
|
||||
.map { db.getCategoriesForManga(it).executeAsBlocking() }
|
||||
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the selected manga from the library.
|
||||
*
|
||||
* @param mangas the list of manga to delete.
|
||||
* @param deleteChapters whether to also delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||
// Create a set of the list
|
||||
val mangaToDelete = mangas.distinctBy { it.id }
|
||||
mangaToDelete.forEach { it.favorite = false }
|
||||
|
||||
Observable.fromCallable { db.insertMangas(mangaToDelete).executeAsBlocking() }
|
||||
.onErrorResumeNext { Observable.empty() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
Observable.fromCallable {
|
||||
mangaToDelete.forEach { manga ->
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
if (deleteChapters) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
downloadManager.findMangaDir(source, manga)?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(categories: List<Category>, mangas: List<Manga>) {
|
||||
val mc = ArrayList<MangaCategory>()
|
||||
|
||||
for (manga in mangas) {
|
||||
for (cat in categories) {
|
||||
mc.add(MangaCategory.create(manga, cat))
|
||||
}
|
||||
}
|
||||
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file.
|
||||
*
|
||||
* @param inputStream the new cover.
|
||||
* @param manga the manga edited.
|
||||
* @return true if the cover is updated, false otherwise
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
|
||||
if (manga.source == LocalSource.ID) {
|
||||
LocalSource.updateCover(context, manga, inputStream)
|
||||
return true
|
||||
}
|
||||
|
||||
if (manga.thumbnail_url != null && manga.favorite) {
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,160 +1,247 @@
|
||||
package eu.kanade.tachiyomi.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.TaskStackBuilder
|
||||
import android.support.v4.view.GravityCompat
|
||||
import android.view.MenuItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val startScreenId by lazy {
|
||||
when (preferences.startScreen()) {
|
||||
1 -> R.id.nav_drawer_library
|
||||
2 -> R.id.nav_drawer_recently_read
|
||||
3 -> R.id.nav_drawer_recent_updates
|
||||
else -> R.id.nav_drawer_library
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
setAppTheme()
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
|
||||
if (!isTaskRoot) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Inflate activity_main.xml.
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Handle Toolbar
|
||||
setupToolbar(toolbar, backNavigation = false)
|
||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp)
|
||||
|
||||
// Set behavior of Navigation drawer
|
||||
nav_view.setNavigationItemSelectedListener { item ->
|
||||
// Make information view invisible
|
||||
empty_view.hide()
|
||||
|
||||
val id = item.itemId
|
||||
|
||||
val oldFragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
||||
if (oldFragment == null || oldFragment.tag.toInt() != id) {
|
||||
when (id) {
|
||||
R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance(), id)
|
||||
R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance(), id)
|
||||
R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
|
||||
R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id)
|
||||
R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id)
|
||||
R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java))
|
||||
R.id.nav_drawer_settings -> {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
|
||||
}
|
||||
}
|
||||
}
|
||||
drawer.closeDrawer(GravityCompat.START)
|
||||
true
|
||||
}
|
||||
|
||||
if (savedState == null) {
|
||||
// Set start screen
|
||||
when (intent.action) {
|
||||
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
|
||||
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
|
||||
SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
|
||||
SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
|
||||
else -> setSelectedDrawerItem(startScreenId)
|
||||
}
|
||||
|
||||
// Show changelog if needed
|
||||
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> drawer.openDrawer(GravityCompat.START)
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
||||
if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
|
||||
drawer.closeDrawers()
|
||||
} else if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
||||
if (resumed) {
|
||||
setSelectedDrawerItem(startScreenId)
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) {
|
||||
if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) {
|
||||
// If database is cleared avoid undefined behavior by recreating the stack.
|
||||
TaskStackBuilder.create(this)
|
||||
.addNextIntent(Intent(this, MainActivity::class.java))
|
||||
.startActivities()
|
||||
} else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
|
||||
// Delay activity recreation to avoid fragment leaks.
|
||||
nav_view.post { recreate() }
|
||||
} else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
|
||||
nav_view.post { recreate() }
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelectedDrawerItem(itemId: Int, triggerAction: Boolean = true) {
|
||||
nav_view.setCheckedItem(itemId)
|
||||
if (triggerAction) {
|
||||
nav_view.menu.performIdentifierAction(itemId, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setFragment(fragment: Fragment, itemId: Int) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.frame_container, fragment, "$itemId")
|
||||
.commit()
|
||||
}
|
||||
|
||||
fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) {
|
||||
if (show) empty_view.show(drawable, textResource) else empty_view.hide()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_OPEN_SETTINGS = 200
|
||||
// Shortcut actions
|
||||
private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||
private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.main
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.v4.view.GravityCompat
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.support.v7.graphics.drawable.DrawerArrowDrawable
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.*
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var router: Router
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var drawerArrow: DrawerArrowDrawable? = null
|
||||
|
||||
private var secondaryDrawer: ViewGroup? = null
|
||||
|
||||
private val startScreenId by lazy {
|
||||
when (preferences.startScreen()) {
|
||||
1 -> R.id.nav_drawer_library
|
||||
2 -> R.id.nav_drawer_recently_read
|
||||
3 -> R.id.nav_drawer_recent_updates
|
||||
else -> R.id.nav_drawer_library
|
||||
}
|
||||
}
|
||||
|
||||
private val tabAnimator by lazy { TabsAnimator(tabs) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setAppTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
|
||||
if (!isTaskRoot) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
drawerArrow = DrawerArrowDrawable(this)
|
||||
drawerArrow?.color = Color.WHITE
|
||||
toolbar.navigationIcon = drawerArrow
|
||||
|
||||
// Set behavior of Navigation drawer
|
||||
nav_view.setNavigationItemSelectedListener { item ->
|
||||
val id = item.itemId
|
||||
|
||||
val currentRoot = router.backstack.firstOrNull()
|
||||
if (currentRoot?.tag()?.toIntOrNull() != id) {
|
||||
when (id) {
|
||||
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
|
||||
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
|
||||
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
|
||||
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
|
||||
R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
|
||||
R.id.nav_drawer_downloads -> {
|
||||
startActivity(Intent(this, DownloadActivity::class.java))
|
||||
}
|
||||
R.id.nav_drawer_settings -> {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
|
||||
}
|
||||
}
|
||||
}
|
||||
drawer.closeDrawer(GravityCompat.START)
|
||||
true
|
||||
}
|
||||
|
||||
val container = findViewById(R.id.controller_container) as ViewGroup
|
||||
|
||||
router = Conductor.attachRouter(this, container, savedInstanceState)
|
||||
if (!router.hasRootController()) {
|
||||
// Set start screen
|
||||
when (intent.action) {
|
||||
SHORTCUT_LIBRARY -> setSelectedDrawerItem(R.id.nav_drawer_library)
|
||||
SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_drawer_recent_updates)
|
||||
SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_drawer_recently_read)
|
||||
SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_drawer_catalogues)
|
||||
SHORTCUT_MANGA -> router.setRoot(
|
||||
RouterTransaction.with(MangaController(intent.extras)))
|
||||
else -> setSelectedDrawerItem(startScreenId)
|
||||
}
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
if (router.backstackSize == 1) {
|
||||
drawer.openDrawer(GravityCompat.START)
|
||||
} else {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
router.addChangeListener(object : ControllerChangeHandler.ControllerChangeListener {
|
||||
override fun onChangeStarted(to: Controller?, from: Controller?, isPush: Boolean,
|
||||
container: ViewGroup, handler: ControllerChangeHandler) {
|
||||
|
||||
syncActivityViewWithController(to, from)
|
||||
}
|
||||
|
||||
override fun onChangeCompleted(to: Controller?, from: Controller?, isPush: Boolean,
|
||||
container: ViewGroup, handler: ControllerChangeHandler) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
syncActivityViewWithController(router.backstack.lastOrNull()?.controller())
|
||||
|
||||
// TODO changelog controller
|
||||
if (savedInstanceState == null) {
|
||||
// Show changelog if needed
|
||||
ChangelogDialogFragment.show(this, preferences, supportFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
nav_view?.setNavigationItemSelectedListener(null)
|
||||
toolbar?.setNavigationOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val backstackSize = router.backstackSize
|
||||
if (drawer.isDrawerOpen(GravityCompat.START) || drawer.isDrawerOpen(GravityCompat.END)) {
|
||||
drawer.closeDrawers()
|
||||
} else if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) {
|
||||
setSelectedDrawerItem(startScreenId)
|
||||
} else if (backstackSize == 1 || !router.handleBack()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelectedDrawerItem(itemId: Int) {
|
||||
if (!isFinishing) {
|
||||
nav_view.setCheckedItem(itemId)
|
||||
nav_view.menu.performIdentifierAction(itemId, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRoot(controller: Controller, id: Int) {
|
||||
router.setRoot(RouterTransaction.with(controller)
|
||||
.popChangeHandler(FadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler())
|
||||
.tag(id.toString()))
|
||||
}
|
||||
|
||||
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {
|
||||
if (from is DialogController || to is DialogController) {
|
||||
return
|
||||
}
|
||||
|
||||
val showHamburger = router.backstackSize == 1
|
||||
if (showHamburger) {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
}
|
||||
|
||||
ObjectAnimator.ofFloat(drawerArrow, "progress", if (showHamburger) 0f else 1f).start()
|
||||
|
||||
if (from is TabbedController) {
|
||||
from.cleanupTabs(tabs)
|
||||
}
|
||||
if (to is TabbedController) {
|
||||
to.configureTabs(tabs)
|
||||
tabAnimator.expand()
|
||||
} else {
|
||||
tabAnimator.collapse()
|
||||
tabs.setupWithViewPager(null)
|
||||
}
|
||||
|
||||
if (from is SecondaryDrawerController) {
|
||||
if (secondaryDrawer != null) {
|
||||
from.cleanupSecondaryDrawer(drawer)
|
||||
drawer.removeView(secondaryDrawer)
|
||||
secondaryDrawer = null
|
||||
}
|
||||
}
|
||||
if (to is SecondaryDrawerController) {
|
||||
secondaryDrawer = to.createSecondaryDrawer(drawer)?.also { drawer.addView(it) }
|
||||
}
|
||||
|
||||
if (to is NoToolbarElevationController) {
|
||||
appbar.disableElevation()
|
||||
} else {
|
||||
appbar.enableElevation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_OPEN_SETTINGS && resultCode != 0) {
|
||||
if (resultCode and SettingsActivity.FLAG_DATABASE_CLEARED != 0) {
|
||||
// If database is cleared avoid undefined behavior by recreating the stack.
|
||||
TaskStackBuilder.create(this)
|
||||
.addNextIntent(Intent(this, MainActivity::class.java))
|
||||
.startActivities()
|
||||
} else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
|
||||
// Delay activity recreation to avoid fragment leaks.
|
||||
nav_view.post { recreate() }
|
||||
} else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
|
||||
nav_view.post { recreate() }
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_OPEN_SETTINGS = 200
|
||||
// Shortcut actions
|
||||
private const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||
private const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||
private const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||
private const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class MangaEvent(val manga: Manga)
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,23 +6,13 @@ import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import kotlinx.android.synthetic.main.item_chapter.view.*
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.*
|
||||
|
||||
class ChapterHolder(
|
||||
private val view: View,
|
||||
private val adapter: ChaptersAdapter)
|
||||
: FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val readColor = view.context.getResourceColor(android.R.attr.textColorHint)
|
||||
private val unreadColor = view.context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
private val bookmarkedColor = view.context.getResourceColor(R.attr.colorAccent)
|
||||
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
|
||||
private val df = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
private val adapter: ChaptersAdapter
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
||||
@ -36,19 +26,19 @@ class ChapterHolder(
|
||||
|
||||
chapter_title.text = when (manga.displayMode) {
|
||||
Manga.DISPLAY_NUMBER -> {
|
||||
val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
context.getString(R.string.display_mode_chapter, formattedNumber)
|
||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
}
|
||||
|
||||
// Set correct text color
|
||||
chapter_title.setTextColor(if (chapter.read) readColor else unreadColor)
|
||||
if (chapter.bookmark) chapter_title.setTextColor(bookmarkedColor)
|
||||
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
|
||||
|
||||
if (chapter.date_upload > 0) {
|
||||
chapter_date.text = df.format(Date(chapter.date_upload))
|
||||
chapter_date.setTextColor(if (chapter.read) readColor else unreadColor)
|
||||
chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
|
||||
chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
} else {
|
||||
chapter_date.text = ""
|
||||
}
|
||||
@ -105,7 +95,7 @@ class ChapterHolder(
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
adapter.menuItemListener(adapterPosition, menuItem)
|
||||
adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -1,50 +1,57 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
|
||||
Chapter by chapter {
|
||||
|
||||
private var _status: Int = 0
|
||||
|
||||
var status: Int
|
||||
get() = download?.status ?: _status
|
||||
set(value) { _status = value }
|
||||
|
||||
@Transient var download: Download? = null
|
||||
|
||||
val isDownloaded: Boolean
|
||||
get() = status == Download.DOWNLOADED
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.item_chapter
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): ChapterHolder {
|
||||
return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ChapterHolder, position: Int, payloads: List<Any?>?) {
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is ChapterItem) {
|
||||
return chapter.id!! == other.chapter.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chapter.id!!.hashCode()
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
|
||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
|
||||
Chapter by chapter {
|
||||
|
||||
private var _status: Int = 0
|
||||
|
||||
var status: Int
|
||||
get() = download?.status ?: _status
|
||||
set(value) { _status = value }
|
||||
|
||||
@Transient var download: Download? = null
|
||||
|
||||
val isDownloaded: Boolean
|
||||
get() = status == Download.DOWNLOADED
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.item_chapter
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): ChapterHolder {
|
||||
|
||||
return ChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: ChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is ChapterItem) {
|
||||
return chapter.id!! == other.chapter.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chapter.id!!.hashCode()
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +1,45 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter<ChapterItem>(null, fragment, true) {
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val menuItemListener: (Int, MenuItem) -> Unit = { position, item ->
|
||||
fragment.onItemMenuClick(position, item)
|
||||
}
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>) {
|
||||
this.items = items
|
||||
super.updateDataSet(items.toList())
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class ChaptersAdapter(
|
||||
controller: ChaptersController,
|
||||
context: Context
|
||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val menuItemListener: OnMenuItemClickListener = controller
|
||||
|
||||
val readColor = context.getResourceColor(android.R.attr.textColorHint)
|
||||
|
||||
val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
|
||||
val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' })
|
||||
|
||||
val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>) {
|
||||
this.items = items
|
||||
super.updateDataSet(items.toList())
|
||||
}
|
||||
|
||||
fun indexOf(item: ChapterItem): Int {
|
||||
return items.indexOf(item)
|
||||
}
|
||||
|
||||
interface OnMenuItemClickListener {
|
||||
fun onMenuItemClick(position: Int, item: MenuItem)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,446 +1,415 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Presenter of [ChaptersFragment].
|
||||
*/
|
||||
class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
||||
|
||||
/**
|
||||
* Database helper.
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Downloads manager.
|
||||
*/
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Active manga.
|
||||
*/
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
/**
|
||||
* Source of the manga.
|
||||
*/
|
||||
lateinit var source: Source
|
||||
private set
|
||||
|
||||
/**
|
||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||
*/
|
||||
var chapters: List<ChapterItem> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subject of list of chapters to allow updating the view without going to DB.
|
||||
*/
|
||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
||||
|
||||
/**
|
||||
* Whether the chapter list has been requested to the source.
|
||||
*/
|
||||
var hasRequested = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription to retrieve the new list of chapters from the source.
|
||||
*/
|
||||
private var fetchChaptersSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Find the active manga from the shared data or return.
|
||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||
source = sourceManager.get(manga.source)!!
|
||||
Observable.just(manga)
|
||||
.subscribeLatestCache(ChaptersFragment::onNextManga)
|
||||
|
||||
// Prepare the relay.
|
||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(ChaptersFragment::onNextChapters,
|
||||
{ _, error -> Timber.e(error) })
|
||||
|
||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||
// changes, and sends the list of chapters to the relay.
|
||||
add(db.getChapters(manga).asRxObservable()
|
||||
.map { chapters ->
|
||||
// Convert every chapter to a model.
|
||||
chapters.map { it.toModel() }
|
||||
}
|
||||
.doOnNext { chapters ->
|
||||
// Find downloaded chapters
|
||||
setDownloadedChapters(chapters)
|
||||
|
||||
// Store the last emission
|
||||
this.chapters = chapters
|
||||
|
||||
// Listen for download status changes
|
||||
observeDownloads()
|
||||
|
||||
// Emit the number of chapters to the info tab.
|
||||
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
|
||||
}
|
||||
.subscribe { chaptersRelay.call(it) })
|
||||
}
|
||||
|
||||
private fun observeDownloads() {
|
||||
observeDownloadsSubscription?.let { remove(it) }
|
||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { download -> download.manga.id == manga.id }
|
||||
.doOnNext { onDownloadStatusChange(it) }
|
||||
.subscribeLatestCache(ChaptersFragment::onChapterStatusChange,
|
||||
{ _, error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||
*/
|
||||
private fun Chapter.toModel(): ChapterItem {
|
||||
// Create the model object.
|
||||
val model = ChapterItem(this, manga)
|
||||
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||
|
||||
if (download != null) {
|
||||
// If there's an active download, assign it.
|
||||
model.download = download
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
|
||||
val cached = mutableMapOf<Chapter, String>()
|
||||
files.mapNotNull { it.name }
|
||||
.mapNotNull { name -> chapters.find {
|
||||
name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) }
|
||||
} }
|
||||
.forEach { it.status = Download.DOWNLOADED }
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an updated list of chapters from the source.
|
||||
*/
|
||||
fun fetchChaptersFromSource() {
|
||||
hasRequested = true
|
||||
|
||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchChaptersDone()
|
||||
}, ChaptersFragment::onFetchChaptersError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI after applying the filters.
|
||||
*/
|
||||
private fun refreshChapters() {
|
||||
chaptersRelay.call(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @param chapters the list of chapters from the database
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||
if (onlyUnread()) {
|
||||
observable = observable.filter { !it.read }
|
||||
}
|
||||
else if (onlyRead()) {
|
||||
observable = observable.filter { it.read }
|
||||
}
|
||||
if (onlyDownloaded()) {
|
||||
observable = observable.filter { it.isDownloaded }
|
||||
}
|
||||
if (onlyBookmarked()) {
|
||||
observable = observable.filter { it.bookmark }
|
||||
}
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
|
||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
}
|
||||
Manga.SORTING_NUMBER -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
return observable.toSortedList(sortFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download for the active manga changes status.
|
||||
* @param download the download whose status changed.
|
||||
*/
|
||||
fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.QUEUE) {
|
||||
chapters.find { it.id == download.chapter.id }?.let {
|
||||
if (it.download == null) {
|
||||
it.download = download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force UI update if downloaded filter active and download finished.
|
||||
if (onlyDownloaded() && download.status == Download.DOWNLOADED)
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unread chapter or null if everything is read.
|
||||
*/
|
||||
fun getNextUnreadChapter(): ChapterItem? {
|
||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the selected chapter list as read/unread.
|
||||
* @param selectedChapters the list of selected chapters.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.read = read
|
||||
if (!read) {
|
||||
chapter.last_page_read = 0
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
DownloadService.start(context)
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param selectedChapters the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.bookmark = bookmarked
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given list of chapter.
|
||||
* @param chapters the list of chapters to delete.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
Observable.from(chapters)
|
||||
.doOnNext { deleteChapter(it) }
|
||||
.toList()
|
||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, ChaptersFragment::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a chapter from disk. This method is called in a background thread.
|
||||
* @param chapter the chapter to delete.
|
||||
*/
|
||||
private fun deleteChapter(chapter: ChapterItem) {
|
||||
downloadManager.queue.remove(chapter)
|
||||
downloadManager.deleteChapter(source, manga, chapter)
|
||||
chapter.status = Download.NOT_DOWNLOADED
|
||||
chapter.download = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the sorting and requests an UI update.
|
||||
*/
|
||||
fun revertSortOrder() {
|
||||
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
||||
*/
|
||||
fun setUnreadFilter(onlyUnread: Boolean) {
|
||||
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyRead whether to display only read chapters or all chapters.
|
||||
*/
|
||||
fun setReadFilter(onlyRead: Boolean) {
|
||||
manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the download filter and requests an UI update.
|
||||
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
|
||||
*/
|
||||
fun setDownloadedFilter(onlyDownloaded: Boolean) {
|
||||
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bookmark filter and requests an UI update.
|
||||
* @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
|
||||
*/
|
||||
fun setBookmarkedFilter(onlyBookmarked: Boolean) {
|
||||
manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all filters and requests an UI update.
|
||||
*/
|
||||
fun removeFilters() {
|
||||
manga.readFilter = Manga.SHOW_ALL
|
||||
manga.downloadedFilter = Manga.SHOW_ALL
|
||||
manga.bookmarkedFilter = Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds manga to library
|
||||
*/
|
||||
fun addToLibrary() {
|
||||
SharedData.get(MangaFavoriteEvent::class.java)?.call(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active display mode.
|
||||
* @param mode the mode to set.
|
||||
*/
|
||||
fun setDisplayMode(mode: Int) {
|
||||
manga.displayMode = mode
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sorting method and requests an UI update.
|
||||
* @param sort the sorting mode.
|
||||
*/
|
||||
fun setSorting(sort: Int) {
|
||||
manga.sorting = sort
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyDownloaded(): Boolean {
|
||||
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyBookmarked(): Boolean {
|
||||
return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only unread filter is enabled.
|
||||
*/
|
||||
fun onlyUnread(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_UNREAD
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only read filter is enabled.
|
||||
*/
|
||||
fun onlyRead(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_READ
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the sorting method is descending or ascending.
|
||||
*/
|
||||
fun sortDescending(): Boolean {
|
||||
return manga.sortDescending()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [ChaptersController].
|
||||
*/
|
||||
class ChaptersPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Int>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<ChaptersController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||
*/
|
||||
var chapters: List<ChapterItem> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subject of list of chapters to allow updating the view without going to DB.
|
||||
*/
|
||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
||||
|
||||
/**
|
||||
* Whether the chapter list has been requested to the source.
|
||||
*/
|
||||
var hasRequested = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription to retrieve the new list of chapters from the source.
|
||||
*/
|
||||
private var fetchChaptersSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Prepare the relay.
|
||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(ChaptersController::onNextChapters,
|
||||
{ _, error -> Timber.e(error) })
|
||||
|
||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||
// changes, and sends the list of chapters to the relay.
|
||||
add(db.getChapters(manga).asRxObservable()
|
||||
.map { chapters ->
|
||||
// Convert every chapter to a model.
|
||||
chapters.map { it.toModel() }
|
||||
}
|
||||
.doOnNext { chapters ->
|
||||
// Find downloaded chapters
|
||||
setDownloadedChapters(chapters)
|
||||
|
||||
// Store the last emission
|
||||
this.chapters = chapters
|
||||
|
||||
// Listen for download status changes
|
||||
observeDownloads()
|
||||
|
||||
// Emit the number of chapters to the info tab.
|
||||
chapterCountRelay.call(chapters.size)
|
||||
}
|
||||
.subscribe { chaptersRelay.call(it) })
|
||||
}
|
||||
|
||||
private fun observeDownloads() {
|
||||
observeDownloadsSubscription?.let { remove(it) }
|
||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { download -> download.manga.id == manga.id }
|
||||
.doOnNext { onDownloadStatusChange(it) }
|
||||
.subscribeLatestCache(ChaptersController::onChapterStatusChange,
|
||||
{ _, error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||
*/
|
||||
private fun Chapter.toModel(): ChapterItem {
|
||||
// Create the model object.
|
||||
val model = ChapterItem(this, manga)
|
||||
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||
|
||||
if (download != null) {
|
||||
// If there's an active download, assign it.
|
||||
model.download = download
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
|
||||
val cached = mutableMapOf<Chapter, String>()
|
||||
files.mapNotNull { it.name }
|
||||
.mapNotNull { name -> chapters.find {
|
||||
name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) }
|
||||
} }
|
||||
.forEach { it.status = Download.DOWNLOADED }
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an updated list of chapters from the source.
|
||||
*/
|
||||
fun fetchChaptersFromSource() {
|
||||
hasRequested = true
|
||||
|
||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchChaptersDone()
|
||||
}, ChaptersController::onFetchChaptersError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI after applying the filters.
|
||||
*/
|
||||
private fun refreshChapters() {
|
||||
chaptersRelay.call(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @param chapters the list of chapters from the database
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||
if (onlyUnread()) {
|
||||
observable = observable.filter { !it.read }
|
||||
}
|
||||
else if (onlyRead()) {
|
||||
observable = observable.filter { it.read }
|
||||
}
|
||||
if (onlyDownloaded()) {
|
||||
observable = observable.filter { it.isDownloaded }
|
||||
}
|
||||
if (onlyBookmarked()) {
|
||||
observable = observable.filter { it.bookmark }
|
||||
}
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
|
||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
}
|
||||
Manga.SORTING_NUMBER -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
return observable.toSortedList(sortFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download for the active manga changes status.
|
||||
* @param download the download whose status changed.
|
||||
*/
|
||||
fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.QUEUE) {
|
||||
chapters.find { it.id == download.chapter.id }?.let {
|
||||
if (it.download == null) {
|
||||
it.download = download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force UI update if downloaded filter active and download finished.
|
||||
if (onlyDownloaded() && download.status == Download.DOWNLOADED)
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unread chapter or null if everything is read.
|
||||
*/
|
||||
fun getNextUnreadChapter(): ChapterItem? {
|
||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the selected chapter list as read/unread.
|
||||
* @param selectedChapters the list of selected chapters.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.read = read
|
||||
if (!read) {
|
||||
chapter.last_page_read = 0
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
DownloadService.start(context)
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param selectedChapters the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.bookmark = bookmarked
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given list of chapter.
|
||||
* @param chapters the list of chapters to delete.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
Observable.from(chapters)
|
||||
.doOnNext { deleteChapter(it) }
|
||||
.toList()
|
||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, ChaptersController::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a chapter from disk. This method is called in a background thread.
|
||||
* @param chapter the chapter to delete.
|
||||
*/
|
||||
private fun deleteChapter(chapter: ChapterItem) {
|
||||
downloadManager.queue.remove(chapter)
|
||||
downloadManager.deleteChapter(source, manga, chapter)
|
||||
chapter.status = Download.NOT_DOWNLOADED
|
||||
chapter.download = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the sorting and requests an UI update.
|
||||
*/
|
||||
fun revertSortOrder() {
|
||||
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
||||
*/
|
||||
fun setUnreadFilter(onlyUnread: Boolean) {
|
||||
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyRead whether to display only read chapters or all chapters.
|
||||
*/
|
||||
fun setReadFilter(onlyRead: Boolean) {
|
||||
manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the download filter and requests an UI update.
|
||||
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
|
||||
*/
|
||||
fun setDownloadedFilter(onlyDownloaded: Boolean) {
|
||||
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bookmark filter and requests an UI update.
|
||||
* @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
|
||||
*/
|
||||
fun setBookmarkedFilter(onlyBookmarked: Boolean) {
|
||||
manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all filters and requests an UI update.
|
||||
*/
|
||||
fun removeFilters() {
|
||||
manga.readFilter = Manga.SHOW_ALL
|
||||
manga.downloadedFilter = Manga.SHOW_ALL
|
||||
manga.bookmarkedFilter = Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds manga to library
|
||||
*/
|
||||
fun addToLibrary() {
|
||||
mangaFavoriteRelay.call(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active display mode.
|
||||
* @param mode the mode to set.
|
||||
*/
|
||||
fun setDisplayMode(mode: Int) {
|
||||
manga.displayMode = mode
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sorting method and requests an UI update.
|
||||
* @param sort the sorting mode.
|
||||
*/
|
||||
fun setSorting(sort: Int) {
|
||||
manga.sorting = sort
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyDownloaded(): Boolean {
|
||||
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyBookmarked(): Boolean {
|
||||
return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only unread filter is enabled.
|
||||
*/
|
||||
fun onlyUnread(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_UNREAD
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only read filter is enabled.
|
||||
*/
|
||||
fun onlyRead(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_READ
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the sorting method is descending or ascending.
|
||||
*/
|
||||
fun sortDescending(): Boolean {
|
||||
return manga.sortDescending()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -1,201 +1,169 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Presenter of MangaInfoFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
||||
|
||||
/**
|
||||
* Active manga.
|
||||
*/
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
/**
|
||||
* Source of the manga.
|
||||
*/
|
||||
lateinit var source: Source
|
||||
private set
|
||||
|
||||
/**
|
||||
* Used to connect to database.
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Used to connect to different manga sources.
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Used to connect to cache.
|
||||
*/
|
||||
val coverCache: CoverCache by injectLazy()
|
||||
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Subscription to send the manga to the view.
|
||||
*/
|
||||
private var viewMangaSubcription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to update the manga from the source.
|
||||
*/
|
||||
private var fetchMangaSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||
source = sourceManager.get(manga.source)!!
|
||||
sendMangaToView()
|
||||
|
||||
// Update chapter count
|
||||
SharedData.get(ChapterCountEvent::class.java)?.observable
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribeLatestCache(MangaInfoFragment::setChapterCount)
|
||||
|
||||
// Update favorite status
|
||||
SharedData.get(MangaFavoriteEvent::class.java)?.let {
|
||||
it.observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setFavorite(it) }
|
||||
.apply { add(this) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the active manga to the view.
|
||||
*/
|
||||
fun sendMangaToView() {
|
||||
viewMangaSubcription?.let { remove(it) }
|
||||
viewMangaSubcription = Observable.just(manga)
|
||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga information from source.
|
||||
*/
|
||||
fun fetchMangaFromSource() {
|
||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { sendMangaToView() }
|
||||
.subscribeFirst({ view, manga ->
|
||||
view.onFetchMangaDone()
|
||||
}, { view, error ->
|
||||
view.onFetchMangaError()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||
*
|
||||
* @return the new status of the manga.
|
||||
*/
|
||||
fun toggleFavorite(): Boolean {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
sendMangaToView()
|
||||
return manga.favorite
|
||||
}
|
||||
|
||||
private fun setFavorite(favorite: Boolean) {
|
||||
if (manga.favorite == favorite) {
|
||||
return
|
||||
}
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
fun hasDownloads(): Boolean {
|
||||
return downloadManager.findMangaDir(source, manga) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the downloads for the manga.
|
||||
*/
|
||||
fun deleteDownloads() {
|
||||
downloadManager.findMangaDir(source, manga)?.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default, and user categories.
|
||||
*
|
||||
* @return List of categories, default plus user categories
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return arrayListOf(Category.createDefault()) + db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if(categories.isEmpty()) {
|
||||
return arrayListOf(Category.createDefault().id).toTypedArray()
|
||||
}
|
||||
return categories.map { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategories(categories: List<Category>, manga: Manga) {
|
||||
val mc = categories.map { MangaCategory.create(manga, it) }
|
||||
|
||||
db.setMangaCategories(mc, arrayListOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param category the selected category.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategory(category: Category, manga: Manga) {
|
||||
moveMangaToCategories(arrayListOf(category), manga)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of MangaInfoFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class MangaInfoPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Int>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<MangaInfoController>() {
|
||||
|
||||
/**
|
||||
* Subscription to send the manga to the view.
|
||||
*/
|
||||
private var viewMangaSubcription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to update the manga from the source.
|
||||
*/
|
||||
private var fetchMangaSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
sendMangaToView()
|
||||
|
||||
// Update chapter count
|
||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(MangaInfoController::setChapterCount)
|
||||
|
||||
// Update favorite status
|
||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setFavorite(it) }
|
||||
.apply { add(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the active manga to the view.
|
||||
*/
|
||||
fun sendMangaToView() {
|
||||
viewMangaSubcription?.let { remove(it) }
|
||||
viewMangaSubcription = Observable.just(manga)
|
||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga information from source.
|
||||
*/
|
||||
fun fetchMangaFromSource() {
|
||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { sendMangaToView() }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchMangaDone()
|
||||
}, { view, _ ->
|
||||
view.onFetchMangaError()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||
*
|
||||
* @return the new status of the manga.
|
||||
*/
|
||||
fun toggleFavorite(): Boolean {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
sendMangaToView()
|
||||
return manga.favorite
|
||||
}
|
||||
|
||||
private fun setFavorite(favorite: Boolean) {
|
||||
if (manga.favorite == favorite) {
|
||||
return
|
||||
}
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
fun hasDownloads(): Boolean {
|
||||
return downloadManager.findMangaDir(source, manga) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the downloads for the manga.
|
||||
*/
|
||||
fun deleteDownloads() {
|
||||
downloadManager.findMangaDir(source, manga)?.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default, and user categories.
|
||||
*
|
||||
* @return List of categories, default plus user categories
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param categories the selected categories.
|
||||
*/
|
||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param category the selected category, or null for default category.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -1,33 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
class TrackAdapter(val fragment: TrackFragment) : RecyclerView.Adapter<TrackHolder>() {
|
||||
|
||||
var items = emptyList<TrackItem>()
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
var onClickListener: (TrackItem) -> Unit = {}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||
val view = parent.inflate(R.layout.item_track)
|
||||
return TrackHolder(view, fragment)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||
holder.onSetValues(items[position])
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
|
||||
|
||||
var items = emptyList<TrackItem>()
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
val rowClickListener: OnRowClickListener = controller
|
||||
|
||||
fun getItem(index: Int): TrackItem? {
|
||||
return items.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||
val view = parent.inflate(R.layout.item_track)
|
||||
return TrackHolder(view, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
interface OnRowClickListener {
|
||||
fun onTitleClick(position: Int)
|
||||
fun onStatusClick(position: Int)
|
||||
fun onChaptersClick(position: Int)
|
||||
fun onScoreClick(position: Int)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,42 +1,41 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.android.synthetic.main.item_track.view.*
|
||||
|
||||
class TrackHolder(private val view: View, private val fragment: TrackFragment)
|
||||
: RecyclerView.ViewHolder(view) {
|
||||
|
||||
private lateinit var item: TrackItem
|
||||
|
||||
init {
|
||||
view.title_container.setOnClickListener { fragment.onTitleClick(item) }
|
||||
view.status_container.setOnClickListener { fragment.onStatusClick(item) }
|
||||
view.chapters_container.setOnClickListener { fragment.onChaptersClick(item) }
|
||||
view.score_container.setOnClickListener { fragment.onScoreClick(item) }
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun onSetValues(item: TrackItem) = with(view) {
|
||||
this@TrackHolder.item = item
|
||||
val track = item.track
|
||||
track_logo.setImageResource(item.service.getLogo())
|
||||
logo.setBackgroundColor(item.service.getLogoColor())
|
||||
if (track != null) {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
track_title.setAllCaps(false)
|
||||
track_title.text = track.title
|
||||
track_chapters.text = "${track.last_chapter_read}/" +
|
||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||
track_status.text = item.service.getStatus(track.status)
|
||||
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||
} else {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||
track_title.setText(R.string.action_edit)
|
||||
track_chapters.text = ""
|
||||
track_score.text = ""
|
||||
track_status.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.android.synthetic.main.item_track.view.*
|
||||
|
||||
class TrackHolder(view: View, adapter: TrackAdapter) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
init {
|
||||
val listener = adapter.rowClickListener
|
||||
view.title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
|
||||
view.status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
|
||||
view.chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
|
||||
view.score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Suppress("DEPRECATION")
|
||||
fun bind(item: TrackItem) = with(itemView) {
|
||||
val track = item.track
|
||||
track_logo.setImageResource(item.service.getLogo())
|
||||
logo.setBackgroundColor(item.service.getLogoColor())
|
||||
if (track != null) {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
track_title.setAllCaps(false)
|
||||
track_title.text = track.title
|
||||
track_chapters.text = "${track.last_chapter_read}/" +
|
||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||
track_status.text = item.service.getStatus(track.status)
|
||||
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||
} else {
|
||||
track_title.setTextAppearance(context, R.style.TextAppearance_Medium_Button)
|
||||
track_title.setText(R.string.action_edit)
|
||||
track_chapters.text = ""
|
||||
track_score.text = ""
|
||||
track_status.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
class TrackItem(val track: Track?, val service: TrackService) {
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
data class TrackItem(val track: Track?, val service: TrackService)
|
||||
|
@ -1,137 +1,129 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class TrackPresenter : BasePresenter<TrackFragment>() {
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
private var trackList: List<TrackItem> = emptyList()
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
var selectedService: TrackService? = null
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
private var refreshSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||
fetchTrackings()
|
||||
}
|
||||
|
||||
fun fetchTrackings() {
|
||||
trackSubscription?.let { remove(it) }
|
||||
trackSubscription = db.getTracks(manga)
|
||||
.asRxObservable()
|
||||
.map { tracks ->
|
||||
loggedServices.map { service ->
|
||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { trackList = it }
|
||||
.subscribeLatestCache(TrackFragment::onNextTrackings)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshSubscription?.let { remove(it) }
|
||||
refreshSubscription = Observable.from(trackList)
|
||||
.filter { it.track != null }
|
||||
.concatMap { item ->
|
||||
item.service.refresh(item.track!!)
|
||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||
.map { item }
|
||||
.onErrorReturn { item }
|
||||
}
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
TrackFragment::onRefreshError)
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
val service = selectedService ?: return
|
||||
|
||||
searchSubscription?.let { remove(it) }
|
||||
searchSubscription = service.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(TrackFragment::onSearchResults,
|
||||
TrackFragment::onSearchResultsError)
|
||||
}
|
||||
|
||||
fun registerTracking(item: Track?) {
|
||||
val service = selectedService ?: return
|
||||
|
||||
if (item != null) {
|
||||
item.manga_id = manga.id!!
|
||||
add(service.bind(item)
|
||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
service.update(track)
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
{ view, error ->
|
||||
view.onRefreshError(error)
|
||||
|
||||
// Restart on error to set old values
|
||||
fetchTrackings()
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatus(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.status = item.service.getStatusList()[index]
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setScore(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.score = item.service.indexToScore(index)
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||
val track = item.track!!
|
||||
track.last_chapter_read = chapterNumber
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class TrackPresenter(
|
||||
val manga: Manga,
|
||||
preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get()
|
||||
) : BasePresenter<TrackController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
private var trackList: List<TrackItem> = emptyList()
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
private var refreshSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
fetchTrackings()
|
||||
}
|
||||
|
||||
fun fetchTrackings() {
|
||||
trackSubscription?.let { remove(it) }
|
||||
trackSubscription = db.getTracks(manga)
|
||||
.asRxObservable()
|
||||
.map { tracks ->
|
||||
loggedServices.map { service ->
|
||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { trackList = it }
|
||||
.subscribeLatestCache(TrackController::onNextTrackings)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshSubscription?.let { remove(it) }
|
||||
refreshSubscription = Observable.from(trackList)
|
||||
.filter { it.track != null }
|
||||
.concatMap { item ->
|
||||
item.service.refresh(item.track!!)
|
||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||
.map { item }
|
||||
.onErrorReturn { item }
|
||||
}
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
TrackController::onRefreshError)
|
||||
}
|
||||
|
||||
fun search(query: String, service: TrackService) {
|
||||
searchSubscription?.let { remove(it) }
|
||||
searchSubscription = service.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(TrackController::onSearchResults,
|
||||
TrackController::onSearchResultsError)
|
||||
}
|
||||
|
||||
fun registerTracking(item: Track?, service: TrackService) {
|
||||
if (item != null) {
|
||||
item.manga_id = manga.id!!
|
||||
add(service.bind(item)
|
||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
service.update(track)
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result -> view.onRefreshDone() },
|
||||
{ view, error ->
|
||||
view.onRefreshError(error)
|
||||
|
||||
// Restart on error to set old values
|
||||
fetchTrackings()
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatus(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.status = item.service.getStatusList()[index]
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setScore(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.score = item.service.indexToScore(index)
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||
val track = item.track!!
|
||||
track.last_chapter_read = chapterNumber
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +1,47 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.item_track_search.view.*
|
||||
import java.util.*
|
||||
|
||||
class TrackSearchAdapter(context: Context)
|
||||
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val track = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.item_track_search)
|
||||
holder = TrackSearchHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<Track>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class TrackSearchHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(track: Track) {
|
||||
view.track_search_title.text = track.title
|
||||
}
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.item_track_search.view.*
|
||||
import java.util.*
|
||||
|
||||
class TrackSearchAdapter(context: Context)
|
||||
: ArrayAdapter<Track>(context, R.layout.item_track_search, ArrayList<Track>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val track = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.item_track_search)
|
||||
holder = TrackSearchHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<Track>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class TrackSearchHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(track: Track) {
|
||||
view.track_search_title.text = track.title
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,119 +1,144 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||
import kotlinx.android.synthetic.main.dialog_track_search.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TrackSearchDialog : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(): TrackSearchDialog {
|
||||
return TrackSearchDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var v: View
|
||||
|
||||
lateinit var adapter: TrackSearchAdapter
|
||||
private set
|
||||
|
||||
private val queryRelay by lazy { PublishRelay.create<String>() }
|
||||
|
||||
private var searchDebounceSubscription: Subscription? = null
|
||||
|
||||
private var selectedItem: Track? = null
|
||||
|
||||
val presenter: TrackPresenter
|
||||
get() = (parentFragment as TrackFragment).presenter
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(context)
|
||||
.customView(R.layout.dialog_track_search, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog1, which -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
v = view
|
||||
|
||||
// Create adapter
|
||||
adapter = TrackSearchAdapter(context)
|
||||
view.track_search_list.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
selectedItem = null
|
||||
view.track_search_list.setOnItemClickListener { parent, viewList, position, id ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = presenter.manga.title
|
||||
view.track_search.append(title)
|
||||
search(title)
|
||||
}
|
||||
|
||||
view.track_search.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
queryRelay.call(s.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Listen to text changes
|
||||
searchDebounceSubscription = queryRelay.debounce(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { it.isNotBlank() }
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
searchDebounceSubscription?.unsubscribe()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
v.progress.visibility = View.VISIBLE
|
||||
v.track_search_list.visibility = View.GONE
|
||||
|
||||
presenter.search(query)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<Track>) {
|
||||
selectedItem = null
|
||||
v.progress.visibility = View.GONE
|
||||
v.track_search_list.visibility = View.VISIBLE
|
||||
adapter.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
v.progress.visibility = View.VISIBLE
|
||||
v.track_search_list.visibility = View.GONE
|
||||
adapter.setItems(emptyList())
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
presenter.registerTracking(selectedItem)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxbinding.widget.itemClicks
|
||||
import com.jakewharton.rxbinding.widget.textChanges
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.dialog_track_search.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TrackSearchDialog : DialogController {
|
||||
|
||||
private var dialogView: View? = null
|
||||
|
||||
private var adapter: TrackSearchAdapter? = null
|
||||
|
||||
private var selectedItem: Track? = null
|
||||
|
||||
private val service: TrackService
|
||||
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
private var searchTextSubscription: Subscription? = null
|
||||
|
||||
private val trackController
|
||||
get() = targetController as TrackController
|
||||
|
||||
constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
|
||||
putInt(KEY_SERVICE, service.id)
|
||||
}) {
|
||||
targetController = target
|
||||
this.service = service
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.customView(R.layout.dialog_track_search, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { _, _ -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
if (subscriptions.isUnsubscribed) {
|
||||
subscriptions = CompositeSubscription()
|
||||
}
|
||||
|
||||
dialogView = dialog.view
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
// Create adapter
|
||||
val adapter = TrackSearchAdapter(view.context)
|
||||
this.adapter = adapter
|
||||
view.track_search_list.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
selectedItem = null
|
||||
|
||||
subscriptions += view.track_search_list.itemClicks().subscribe { position ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = trackController.presenter.manga.title
|
||||
view.track_search.append(title)
|
||||
search(title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
subscriptions.unsubscribe()
|
||||
dialogView = null
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
searchTextSubscription = dialogView!!.track_search.textChanges()
|
||||
.skip(1)
|
||||
.debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
|
||||
.map { it.toString() }
|
||||
.filter(String::isNotBlank)
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
searchTextSubscription?.unsubscribe()
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.GONE
|
||||
|
||||
trackController.presenter.search(query, service)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<Track>) {
|
||||
selectedItem = null
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.GONE
|
||||
view.track_search_list.visibility = View.VISIBLE
|
||||
adapter?.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.GONE
|
||||
adapter?.setItems(emptyList())
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
trackController.presenter.registerTracking(selectedItem, service)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_SERVICE = "service_id"
|
||||
}
|
||||
|
||||
}
|
@ -28,7 +28,8 @@ import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.net.URLConnection
|
||||
import java.util.*
|
||||
@ -36,41 +37,17 @@ import java.util.*
|
||||
/**
|
||||
* Presenter of [ReaderActivity].
|
||||
*/
|
||||
class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
val prefs: PreferencesHelper by injectLazy()
|
||||
class ReaderPresenter(
|
||||
val prefs: PreferencesHelper = Injekt.get(),
|
||||
val db: DatabaseHelper = Injekt.get(),
|
||||
val downloadManager: DownloadManager = Injekt.get(),
|
||||
val trackManager: TrackManager = Injekt.get(),
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
val chapterCache: ChapterCache = Injekt.get(),
|
||||
val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<ReaderActivity>() {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Tracking manager.
|
||||
*/
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Chapter cache.
|
||||
*/
|
||||
val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
*/
|
||||
val coverCache: CoverCache by injectLazy()
|
||||
private val context = prefs.context
|
||||
|
||||
/**
|
||||
* Manga being read.
|
||||
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.base
|
||||
|
||||
import android.support.v4.app.Fragment
|
||||
import com.davemorrissey.labs.subscaleview.decoder.*
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import java.util.*
|
||||
@ -12,7 +12,7 @@ import java.util.*
|
||||
* Base reader containing the common data that can be used by its implementations. It does not
|
||||
* contain any UI related action.
|
||||
*/
|
||||
abstract class BaseReader : BaseFragment() {
|
||||
abstract class BaseReader : Fragment() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -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>)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -115,7 +115,7 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
with(adapter.fragment) {
|
||||
with(adapter.controller) {
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_download -> downloadChapter(item)
|
||||
R.id.action_delete -> deleteChapter(item)
|
||||
|
@ -27,11 +27,19 @@ class RecentChapterItem(val chapter: Chapter, val manga: Manga, header: DateItem
|
||||
return R.layout.item_recent_chapters
|
||||
}
|
||||
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): RecentChapterHolder {
|
||||
return RecentChapterHolder(inflater.inflate(layoutRes, parent, false), adapter as RecentChaptersAdapter)
|
||||
override fun createViewHolder(adapter: FlexibleAdapter<*>,
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup): RecentChapterHolder {
|
||||
|
||||
val view = inflater.inflate(layoutRes, parent, false)
|
||||
return RecentChapterHolder(view , adapter as RecentChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: RecentChapterHolder, position: Int, payloads: List<Any?>?) {
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<*>,
|
||||
holder: RecentChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.recent_updates
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
||||
class RecentChaptersAdapter(val fragment: RecentChaptersFragment) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, fragment, true) {
|
||||
class RecentChaptersAdapter(val controller: RecentChaptersController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
|
@ -1,340 +1,323 @@
|
||||
package eu.kanade.tachiyomi.ui.recent_updates
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.DividerItemDecoration
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.*
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.DeletingChaptersDialog
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_recent_chapters.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
* Uses [R.layout.fragment_recent_chapters].
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
@RequiresPresenter(RecentChaptersPresenter::class)
|
||||
class RecentChaptersFragment:
|
||||
BaseRxFragment<RecentChaptersPresenter>(),
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener{
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create new RecentChaptersFragment.
|
||||
* @return a new instance of [RecentChaptersFragment].
|
||||
*/
|
||||
fun newInstance(): RecentChaptersFragment {
|
||||
return RecentChaptersFragment()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Adapter containing the recent chapters.
|
||||
*/
|
||||
lateinit var adapter: RecentChaptersAdapter
|
||||
private set
|
||||
|
||||
/**
|
||||
* Called when view gets created
|
||||
* @param inflater layout inflater
|
||||
* @param container view group
|
||||
* @param savedState status of saved state
|
||||
*/
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
||||
// Inflate view
|
||||
return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when view is created
|
||||
* @param view created view
|
||||
* @param savedState status of saved sate
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
// Init RecyclerView and adapter
|
||||
recycler.layoutManager = LinearLayoutManager(activity)
|
||||
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
recycler.setHasFixedSize(true)
|
||||
adapter = RecentChaptersAdapter(this)
|
||||
recycler.adapter = adapter
|
||||
|
||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recycler: RecyclerView, newState: Int) {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos == 0
|
||||
}
|
||||
})
|
||||
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.setOnRefreshListener {
|
||||
if (!LibraryUpdateService.isRunning(activity)) {
|
||||
LibraryUpdateService.start(activity)
|
||||
context.toast(R.string.action_update_library)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
|
||||
// Update toolbar text
|
||||
setToolbarTitle(R.string.label_recent_updates)
|
||||
|
||||
// Disable toolbar elevation, it looks better with sticky headers.
|
||||
activity.appbar.disableElevation()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
// Restore toolbar elevation.
|
||||
activity.appbar.enableElevation()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected chapters
|
||||
* @return list of selected chapters
|
||||
*/
|
||||
fun getSelectedChapters(): List<RecentChapterItem> {
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
// Get item from position
|
||||
val item = adapter.getItem(position) as? RecentChapterItem ?: return false
|
||||
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openChapter(item)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is long clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
if (actionMode == null)
|
||||
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to toggle selection
|
||||
* @param position position of selected item
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
adapter.toggleSelection(position)
|
||||
|
||||
val count = adapter.selectedItemCount
|
||||
if (count == 0) {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
actionMode?.title = getString(R.string.label_selected, count)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open chapter in reader
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private fun openChapter(item: RecentChapterItem) {
|
||||
val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected items
|
||||
* @param chapters list of selected [RecentChapter]s
|
||||
*/
|
||||
fun downloadChapters(chapters: List<RecentChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.downloadChapters(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
* @param chapters list of [Any]
|
||||
*/
|
||||
fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
|
||||
(activity as MainActivity).updateEmptyView(chapters.isEmpty(),
|
||||
R.string.information_no_recent, R.drawable.ic_update_black_128dp)
|
||||
|
||||
destroyActionModeIfNeeded()
|
||||
adapter.updateDataSet(chapters.toMutableList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
fun onChapterStatusChange(download: Download) {
|
||||
getHolder(download)?.notifyStatus(download.status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns holder belonging to chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
private fun getHolder(download: Download): RecentChapterHolder? {
|
||||
return recycler.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
fun markAsRead(chapters: List<RecentChapterItem>) {
|
||||
presenter.markChapterRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapters
|
||||
* @param chapters list of [RecentChapter] objects
|
||||
*/
|
||||
fun deleteChapters(chapters: List<RecentChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
|
||||
presenter.deleteChapters(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destory [ActionMode] if it's shown
|
||||
*/
|
||||
fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
* @param chapters list of selected [RecentChapter]
|
||||
*/
|
||||
fun markAsUnread(chapters: List<RecentChapterItem>) {
|
||||
presenter.markChapterRead(chapters, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start downloading chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun downloadChapter(chapter: RecentChapterItem) {
|
||||
presenter.downloadChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start deleting chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun deleteChapter(chapter: RecentChapterItem) {
|
||||
DeletingChaptersDialog().show(childFragmentManager, DeletingChaptersDialog.TAG)
|
||||
presenter.deleteChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapters are deleted
|
||||
*/
|
||||
fun onChaptersDeleted() {
|
||||
dismissDeletingDialog()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when error while deleting
|
||||
* @param error error message
|
||||
*/
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
dismissDeletingDialog()
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to dismiss deleting dialog
|
||||
*/
|
||||
fun dismissDeletingDialog() {
|
||||
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)
|
||||
?.dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode item clicked
|
||||
* @param mode the ActionMode object
|
||||
* @param item item from ActionMode.
|
||||
*/
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
if (!isAdded) return true
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
||||
R.id.action_delete -> {
|
||||
MaterialDialog.Builder(activity)
|
||||
.content(R.string.confirm_delete_chapters)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { dialog, action -> deleteChapters(getSelectedChapters()) }
|
||||
.show()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode created.
|
||||
* @param mode the ActionMode object
|
||||
* @param menu menu object of ActionMode
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
|
||||
adapter.mode = FlexibleAdapter.MODE_MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode destroyed
|
||||
* @param mode the ActionMode object
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
adapter.mode = FlexibleAdapter.MODE_IDLE
|
||||
adapter.clearSelection()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.recent_updates
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.DividerItemDecoration
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.fragment_recent_chapters.view.*
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
* Uses [R.layout.fragment_recent_chapters].
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
|
||||
NoToolbarElevationController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
ConfirmDeleteChaptersDialog.Listener {
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Adapter containing the recent chapters.
|
||||
*/
|
||||
var adapter: RecentChaptersAdapter? = null
|
||||
private set
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_updates)
|
||||
}
|
||||
|
||||
override fun createPresenter(): RecentChaptersPresenter {
|
||||
return RecentChaptersPresenter()
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.fragment_recent_chapters, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when view is created
|
||||
* @param view created view
|
||||
* @param savedViewState status of saved sate
|
||||
*/
|
||||
override fun onViewCreated(view: View, savedViewState: Bundle?) {
|
||||
super.onViewCreated(view, savedViewState)
|
||||
|
||||
with(view) {
|
||||
// Init RecyclerView and adapter
|
||||
val layoutManager = LinearLayoutManager(context)
|
||||
recycler.layoutManager = layoutManager
|
||||
recycler.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
recycler.setHasFixedSize(true)
|
||||
adapter = RecentChaptersAdapter(this@RecentChaptersController)
|
||||
recycler.adapter = adapter
|
||||
|
||||
recycler.scrollStateChanges().subscribeUntilDestroy {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos == 0
|
||||
}
|
||||
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy {
|
||||
if (!LibraryUpdateService.isRunning(context)) {
|
||||
LibraryUpdateService.start(context)
|
||||
context.toast(R.string.action_update_library)
|
||||
}
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
adapter = null
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns selected chapters
|
||||
* @return list of selected chapters
|
||||
*/
|
||||
fun getSelectedChapters(): List<RecentChapterItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) as? RecentChapterItem }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
// Get item from position
|
||||
val item = adapter.getItem(position) as? RecentChapterItem ?: return false
|
||||
if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openChapter(item)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item in list is long clicked
|
||||
* @param position position of clicked item
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
if (actionMode == null)
|
||||
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to toggle selection
|
||||
* @param position position of selected item
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
adapter.toggleSelection(position)
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open chapter in reader
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private fun openChapter(item: RecentChapterItem) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, item.manga, item.chapter)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected items
|
||||
* @param chapters list of selected [RecentChapter]s
|
||||
*/
|
||||
fun downloadChapters(chapters: List<RecentChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.downloadChapters(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
* @param chapters list of [Any]
|
||||
*/
|
||||
fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
|
||||
destroyActionModeIfNeeded()
|
||||
adapter?.updateDataSet(chapters.toMutableList())
|
||||
}
|
||||
|
||||
override fun onUpdateEmptyView(size: Int) {
|
||||
val emptyView = view?.empty_view ?: return
|
||||
if (size > 0) {
|
||||
emptyView.hide()
|
||||
} else {
|
||||
emptyView.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
fun onChapterStatusChange(download: Download) {
|
||||
getHolder(download)?.notifyStatus(download.status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns holder belonging to chapter
|
||||
* @param download [Download] object containing download progress.
|
||||
*/
|
||||
private fun getHolder(download: Download): RecentChapterHolder? {
|
||||
return view?.recycler?.findViewHolderForItemId(download.chapter.id!!) as? RecentChapterHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
fun markAsRead(chapters: List<RecentChapterItem>) {
|
||||
presenter.markChapterRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteChapters(chaptersToDelete: List<RecentChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(chaptersToDelete)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destory [ActionMode] if it's shown
|
||||
*/
|
||||
fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
* @param chapters list of selected [RecentChapter]
|
||||
*/
|
||||
fun markAsUnread(chapters: List<RecentChapterItem>) {
|
||||
presenter.markChapterRead(chapters, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start downloading chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun downloadChapter(chapter: RecentChapterItem) {
|
||||
presenter.downloadChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Start deleting chapter
|
||||
* @param chapter selected chapter with manga
|
||||
*/
|
||||
fun deleteChapter(chapter: RecentChapterItem) {
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(listOf(chapter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapters are deleted
|
||||
*/
|
||||
fun onChaptersDeleted() {
|
||||
dismissDeletingDialog()
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when error while deleting
|
||||
* @param error error message
|
||||
*/
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
dismissDeletingDialog()
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to dismiss deleting dialog
|
||||
*/
|
||||
fun dismissDeletingDialog() {
|
||||
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode created.
|
||||
* @param mode the ActionMode object
|
||||
* @param menu menu object of ActionMode
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.chapter_recent_selection, menu)
|
||||
adapter?.mode = FlexibleAdapter.MODE_MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter?.selectedItemCount ?: 0
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = resources?.getString(R.string.label_selected, count)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode item clicked
|
||||
* @param mode the ActionMode object
|
||||
* @param item item from ActionMode.
|
||||
*/
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
||||
R.id.action_delete -> ConfirmDeleteChaptersDialog(this, getSelectedChapters())
|
||||
.showDialog(router)
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode destroyed
|
||||
* @param mode the ActionMode object
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
adapter?.mode = FlexibleAdapter.MODE_IDLE
|
||||
adapter?.clearSelection()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
}
|
@ -14,29 +14,18 @@ import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
/**
|
||||
* Used to connect to database
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
class RecentChaptersPresenter(
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
) : BasePresenter<RecentChaptersController>() {
|
||||
|
||||
/**
|
||||
* Used to get settings
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Used to get information from download manager
|
||||
*/
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Used to get source from source id
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
private val context = preferences.context
|
||||
|
||||
/**
|
||||
* List containing chapter and manga information
|
||||
@ -48,11 +37,11 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
|
||||
getRecentChaptersObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(RecentChaptersFragment::onNextRecentChapters)
|
||||
.subscribeLatestCache(RecentChaptersController::onNextRecentChapters)
|
||||
|
||||
getChapterStatusObservable()
|
||||
.subscribeLatestCache(RecentChaptersFragment::onChapterStatusChange,
|
||||
{ view, error -> Timber.e(error) })
|
||||
.subscribeLatestCache(RecentChaptersController::onChapterStatusChange,
|
||||
{ _, error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,9 +196,9 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, result ->
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, RecentChaptersFragment::onChaptersDeletedError)
|
||||
}, RecentChaptersController::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,57 +1,48 @@
|
||||
package eu.kanade.tachiyomi.ui.recently_read
|
||||
|
||||
import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
/**
|
||||
* Adapter of RecentlyReadHolder.
|
||||
* Connection between Fragment and Holder
|
||||
* Holder updates should be called from here.
|
||||
*
|
||||
* @param fragment a RecentlyReadFragment object
|
||||
* @param controller a RecentlyReadController object
|
||||
* @constructor creates an instance of the adapter.
|
||||
*/
|
||||
class RecentlyReadAdapter(val fragment: RecentlyReadFragment)
|
||||
: FlexibleAdapter<RecentlyReadHolder, MangaChapterHistory>() {
|
||||
class RecentlyReadAdapter(controller: RecentlyReadController)
|
||||
: FlexibleAdapter<RecentlyReadItem>(null, controller, true) {
|
||||
|
||||
val sourceManager by injectLazy<SourceManager>()
|
||||
|
||||
/**
|
||||
* Called when ViewHolder is created
|
||||
* @param parent parent View
|
||||
* @param viewType int containing viewType
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentlyReadHolder {
|
||||
val view = parent.inflate(R.layout.item_recently_read)
|
||||
return RecentlyReadHolder(view, this)
|
||||
}
|
||||
val resumeClickListener: OnResumeClickListener = controller
|
||||
|
||||
val removeClickListener: OnRemoveClickListener = controller
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
/**
|
||||
* Called when ViewHolder is bind
|
||||
* @param holder bind holder
|
||||
* @param position position of holder
|
||||
* DecimalFormat used to display correct chapter number
|
||||
*/
|
||||
override fun onBindViewHolder(holder: RecentlyReadHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.onSetValues(item)
|
||||
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' })
|
||||
|
||||
val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||
|
||||
interface OnResumeClickListener {
|
||||
fun onResumeClick(position: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update items
|
||||
* @param items items
|
||||
*/
|
||||
fun setItems(items: List<MangaChapterHistory>) {
|
||||
mItems = items
|
||||
notifyDataSetChanged()
|
||||
interface OnRemoveClickListener {
|
||||
fun onRemoveClick(position: Int)
|
||||
}
|
||||
|
||||
override fun updateDataSet(param: String?) {
|
||||
// Empty function
|
||||
interface OnCoverClickListener {
|
||||
fun onCoverClick(position: Int)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -1,17 +1,12 @@
|
||||
package eu.kanade.tachiyomi.ui.recently_read
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.widget.DialogCheckboxView
|
||||
import kotlinx.android.synthetic.main.item_recently_read.view.*
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -23,39 +18,47 @@ import java.util.*
|
||||
* @param adapter the adapter handling this holder.
|
||||
* @constructor creates a new recent chapter holder.
|
||||
*/
|
||||
class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
|
||||
: RecyclerView.ViewHolder(view) {
|
||||
class RecentlyReadHolder(
|
||||
view: View,
|
||||
val adapter: RecentlyReadAdapter
|
||||
) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
/**
|
||||
* DecimalFormat used to display correct chapter number
|
||||
*/
|
||||
private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
|
||||
init {
|
||||
itemView.remove.setOnClickListener {
|
||||
adapter.removeClickListener.onRemoveClick(adapterPosition)
|
||||
}
|
||||
|
||||
private val df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||
itemView.resume.setOnClickListener {
|
||||
adapter.resumeClickListener.onResumeClick(adapterPosition)
|
||||
}
|
||||
|
||||
itemView.cover.setOnClickListener {
|
||||
adapter.coverClickListener.onCoverClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set values of view
|
||||
*
|
||||
* @param item item containing history information
|
||||
*/
|
||||
fun onSetValues(item: MangaChapterHistory) {
|
||||
fun bind(item: MangaChapterHistory) {
|
||||
// Retrieve objects
|
||||
val manga = item.manga
|
||||
val chapter = item.chapter
|
||||
val history = item.history
|
||||
val (manga, chapter, history) = item
|
||||
|
||||
// Set manga title
|
||||
itemView.manga_title.text = manga.title
|
||||
|
||||
// Set source + chapter title
|
||||
val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source)
|
||||
.format(adapter.sourceManager.get(manga.source)?.toString(), formattedNumber)
|
||||
|
||||
// Set last read timestamp title
|
||||
itemView.last_read.text = df.format(Date(history.last_read))
|
||||
itemView.last_read.text = adapter.dateFormat.format(Date(history.last_read))
|
||||
|
||||
// Set cover
|
||||
Glide.clear(itemView.cover)
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
Glide.with(itemView.context)
|
||||
.load(manga)
|
||||
@ -64,40 +67,6 @@ class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter)
|
||||
.into(itemView.cover)
|
||||
}
|
||||
|
||||
// Set remove clickListener
|
||||
itemView.remove.setOnClickListener {
|
||||
// Create custom view
|
||||
val dialogCheckboxView = DialogCheckboxView(itemView.context).apply {
|
||||
setDescription(R.string.dialog_with_checkbox_remove_description)
|
||||
setOptionDescription(R.string.dialog_with_checkbox_reset)
|
||||
}
|
||||
MaterialDialog.Builder(itemView.context)
|
||||
.title(R.string.action_remove)
|
||||
.customView(dialogCheckboxView, true)
|
||||
.positiveText(R.string.action_remove)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { materialDialog, dialogAction ->
|
||||
// Check if user wants all chapters reset
|
||||
if (dialogCheckboxView.isChecked()) {
|
||||
adapter.fragment.removeAllFromHistory(manga.id!!)
|
||||
} else {
|
||||
adapter.fragment.removeFromHistory(history)
|
||||
}
|
||||
}
|
||||
.onNegative { materialDialog, dialogAction ->
|
||||
materialDialog.dismiss()
|
||||
}.show()
|
||||
}
|
||||
|
||||
// Set continue reading clickListener
|
||||
itemView.resume.setOnClickListener {
|
||||
adapter.fragment.openChapter(chapter, manga)
|
||||
}
|
||||
|
||||
// Set open manga info clickListener
|
||||
itemView.cover.setOnClickListener {
|
||||
adapter.fragment.openMangaInfo(manga)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -5,11 +5,9 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
@ -18,7 +16,7 @@ import java.util.*
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
|
||||
class RecentlyReadPresenter : BasePresenter<RecentlyReadController>() {
|
||||
|
||||
/**
|
||||
* Used to connect to database
|
||||
@ -30,22 +28,21 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
|
||||
|
||||
// Used to get a list of recently read manga
|
||||
getRecentMangaObservable()
|
||||
.subscribeLatestCache({ view, historyList ->
|
||||
view.onNextManga(historyList)
|
||||
})
|
||||
.subscribeLatestCache(RecentlyReadController::onNextManga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent manga observable
|
||||
* @return list of history
|
||||
*/
|
||||
fun getRecentMangaObservable(): Observable<List<MangaChapterHistory>> {
|
||||
fun getRecentMangaObservable(): Observable<List<RecentlyReadItem>> {
|
||||
// Set date for recent manga
|
||||
val cal = Calendar.getInstance()
|
||||
cal.time = Date()
|
||||
cal.add(Calendar.MONTH, -1)
|
||||
|
||||
return db.getRecentManga(cal.time).asRxObservable()
|
||||
.map { it.map(::RecentlyReadItem) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@ -73,50 +70,39 @@ class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the next chapter instead of the current one.
|
||||
* Retrieves the next chapter of the given one.
|
||||
*
|
||||
* @param chapter the chapter of the history object.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
fun openNextChapter(chapter: Chapter, manga: Manga) {
|
||||
fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
|
||||
if (!chapter.read) {
|
||||
return chapter
|
||||
}
|
||||
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
db.getChapters(manga).asRxSingle()
|
||||
.map { it.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) }) }
|
||||
.map { chapters ->
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> {
|
||||
chapters.getOrNull(currChapterIndex + 1)
|
||||
}
|
||||
Manga.SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapter_number
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
|
||||
|
||||
var nextChapter: Chapter? = null
|
||||
for (i in (currChapterIndex + 1) until chapters.size) {
|
||||
val c = chapters[i]
|
||||
if (c.chapter_number > chapterNumber &&
|
||||
c.chapter_number <= chapterNumber + 1) {
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
|
||||
Manga.SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapter_number
|
||||
|
||||
nextChapter = c
|
||||
break
|
||||
}
|
||||
}
|
||||
nextChapter
|
||||
((currChapterIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull { it.chapter_number > chapterNumber &&
|
||||
it.chapter_number <= chapterNumber + 1
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
.toObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, chapter ->
|
||||
view.onOpenNextChapter(chapter, manga)
|
||||
}, { view, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,6 @@ import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.support.v4.view.PagerAdapter
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter
|
||||
import java.util.*
|
||||
|
||||
abstract class RecyclerViewPagerAdapter : PagerAdapter() {
|
||||
abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
|
||||
|
||||
private val pool = Stack<View>()
|
||||
|
||||
@ -21,22 +21,16 @@ abstract class RecyclerViewPagerAdapter : PagerAdapter() {
|
||||
|
||||
protected open fun recycleView(view: View, position: Int) {}
|
||||
|
||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||
override fun createView(container: ViewGroup, position: Int): View {
|
||||
val view = if (pool.isNotEmpty()) pool.pop() else createView(container)
|
||||
bindView(view, position)
|
||||
container.addView(view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
|
||||
val view = obj as View
|
||||
override fun destroyView(container: ViewGroup, position: Int, view: View) {
|
||||
recycleView(view, position)
|
||||
container.removeView(view)
|
||||
if (recycle) pool.push(view)
|
||||
}
|
||||
|
||||
override fun isViewFromObject(view: View, obj: Any): Boolean {
|
||||
return view === obj
|
||||
}
|
||||
|
||||
}
|
281
app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java
Normal file
281
app/src/main/java/eu/kanade/tachiyomi/widget/UndoHelper.java
Normal 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<T> 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);
|
||||
}
|
||||
|
||||
}
|
@ -2,27 +2,17 @@
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
|
||||
android:id="@+id/controller_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:id="@+id/recycler"
|
||||
android:choiceMode="multipleChoice"
|
||||
tools:listitem="@layout/item_edit_categories"
|
||||
/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
app:layout_anchor="@id/recycler"
|
||||
app:srcCompat="@drawable/ic_add_white_24dp"
|
||||
style="@style/Theme.Widget.FAB"/>
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user