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