diff --git a/app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java b/app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java index 9f1ef30553..f0ece368ae 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/database/DatabaseHelper.java @@ -2,15 +2,18 @@ package eu.kanade.mangafeed.data.database; import android.content.Context; +import com.pushtorefresh.storio.Queries; import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping; import com.pushtorefresh.storio.sqlite.StorIOSQLite; import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite; +import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery; import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteCollectionOfObjects; import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteObject; import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects; import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects; import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject; import com.pushtorefresh.storio.sqlite.operations.put.PutResults; +import com.pushtorefresh.storio.sqlite.queries.DeleteQuery; import com.pushtorefresh.storio.sqlite.queries.Query; import com.pushtorefresh.storio.sqlite.queries.RawQuery; @@ -386,9 +389,36 @@ public class DatabaseHelper { .prepare(); } - public PreparedPutCollectionOfObjects insertMangasCategory(List mangasCategory) { + public PreparedPutCollectionOfObjects insertMangasCategories(List mangasCategories) { return db.put() - .objects(mangasCategory) + .objects(mangasCategories) .prepare(); } + + public PreparedDeleteByQuery deleteOldMangasCategories(List mangas) { + List mangaIds = Observable.from(mangas) + .map(manga -> manga.id) + .toList().toBlocking().single(); + + return db.delete() + .byQuery(DeleteQuery.builder() + .table(MangaCategoryTable.TABLE) + .where(MangaCategoryTable.COLUMN_MANGA_ID + " IN (" + + Queries.placeholders(mangas.size()) + ")") + .whereArgs(mangaIds.toArray()) + .build()) + .prepare(); + } + + public void setMangaCategories(List mangasCategories, List mangas) { + db.internal().beginTransaction(); + try { + deleteOldMangasCategories(mangas).executeAsBlocking(); + insertMangasCategories(mangasCategories).executeAsBlocking(); + db.internal().setTransactionSuccessful(); + } finally { + db.internal().endTransaction(); + } + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/SmartFragmentStatePagerAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/SmartFragmentStatePagerAdapter.java similarity index 77% rename from app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/SmartFragmentStatePagerAdapter.java rename to app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/SmartFragmentStatePagerAdapter.java index 85739d0c93..22bb6717d9 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/SmartFragmentStatePagerAdapter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/base/adapter/SmartFragmentStatePagerAdapter.java @@ -1,4 +1,4 @@ -package eu.kanade.mangafeed.ui.reader.viewer.common; +package eu.kanade.mangafeed.ui.base.adapter; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; @@ -6,6 +6,9 @@ import android.support.v4.app.FragmentStatePagerAdapter; import android.util.SparseArray; import android.view.ViewGroup; +import java.util.ArrayList; +import java.util.List; + public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerAdapter { // Sparse array to keep track of registered fragments in memory private SparseArray registeredFragments = new SparseArray(); @@ -33,4 +36,13 @@ public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerA public Fragment getRegisteredFragment(int position) { return registeredFragments.get(position); } + + public List getRegisteredFragments() { + ArrayList fragments = new ArrayList<>(); + for (int i = 0; i < registeredFragments.size(); i++) { + fragments.add(registeredFragments.valueAt(i)); + } + return fragments; + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryAdapter.java index c5ca4f7f3e..688b3c0ea4 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryAdapter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryAdapter.java @@ -2,15 +2,15 @@ package eu.kanade.mangafeed.ui.library; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; import java.util.List; import eu.kanade.mangafeed.data.database.models.Category; +import eu.kanade.mangafeed.ui.base.adapter.SmartFragmentStatePagerAdapter; -class LibraryAdapter extends FragmentStatePagerAdapter { +public class LibraryAdapter extends SmartFragmentStatePagerAdapter { - private List categories; + protected List categories; public LibraryAdapter(FragmentManager fm) { super(fm); @@ -18,8 +18,7 @@ class LibraryAdapter extends FragmentStatePagerAdapter { @Override public Fragment getItem(int position) { - Category category = categories.get(position); - return LibraryCategoryFragment.newInstance(category); + return LibraryCategoryFragment.newInstance(position); } @Override @@ -37,4 +36,10 @@ class LibraryAdapter extends FragmentStatePagerAdapter { notifyDataSetChanged(); } + public void setSelectionMode(int mode) { + for (Fragment fragment : getRegisteredFragments()) { + ((LibraryCategoryFragment) fragment).setMode(mode); + } + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryCategoryFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryCategoryFragment.java index 8fd3f7b94e..e5cd2c2f45 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryCategoryFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryCategoryFragment.java @@ -3,24 +3,22 @@ package eu.kanade.mangafeed.ui.library; import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; -import android.support.v7.view.ActionMode; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import com.f2prateek.rx.preferences.Preference; +import java.util.ArrayList; import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.database.models.Category; import eu.kanade.mangafeed.data.database.models.Manga; import eu.kanade.mangafeed.event.LibraryMangasEvent; -import eu.kanade.mangafeed.ui.base.activity.BaseActivity; import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder; import eu.kanade.mangafeed.ui.base.fragment.BaseFragment; import eu.kanade.mangafeed.ui.manga.MangaActivity; @@ -30,20 +28,19 @@ import icepick.Icepick; import icepick.State; import rx.Subscription; -public class LibraryCategoryFragment extends BaseFragment implements - ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { +public class LibraryCategoryFragment extends BaseFragment + implements FlexibleViewHolder.OnListItemClickListener { @Bind(R.id.library_mangas) AutofitRecyclerView recycler; - @State Category category; + @State int position; private LibraryCategoryAdapter adapter; - private ActionMode actionMode; private Subscription numColumnsSubscription; - public static LibraryCategoryFragment newInstance(Category category) { + public static LibraryCategoryFragment newInstance(int position) { LibraryCategoryFragment fragment = new LibraryCategoryFragment(); - fragment.category = category; + fragment.position = position; return fragment; } @@ -54,11 +51,14 @@ public class LibraryCategoryFragment extends BaseFragment implements ButterKnife.bind(this, view); Icepick.restoreInstanceState(this, savedState); - recycler.setHasFixedSize(true); - adapter = new LibraryCategoryAdapter(this); + recycler.setHasFixedSize(true); recycler.setAdapter(adapter); + if (getLibraryFragment().getActionMode() != null) { + setMode(FlexibleAdapter.MODE_MULTI); + } + Preference columnsPref = getResources().getConfiguration() .orientation == Configuration.ORIENTATION_PORTRAIT ? getLibraryPresenter().preferences.portraitColumns() : @@ -67,6 +67,14 @@ public class LibraryCategoryFragment extends BaseFragment implements numColumnsSubscription = columnsPref.asObservable() .subscribe(recycler::setSpanCount); + if (savedState != null) { + adapter.onRestoreInstanceState(savedState); + + if (adapter.getMode() == FlexibleAdapter.MODE_SINGLE) { + adapter.clearSelection(); + } + } + return view; } @@ -91,13 +99,23 @@ public class LibraryCategoryFragment extends BaseFragment implements @Override public void onSaveInstanceState(Bundle outState) { Icepick.saveInstanceState(this, outState); + adapter.onSaveInstanceState(outState); super.onSaveInstanceState(outState); } @EventBusHook public void onEventMainThread(LibraryMangasEvent event) { - destroyActionModeIfNeeded(); - setMangas(event.getMangas().get(category.id)); + List categories = getLibraryFragment().getAdapter().categories; + // When a category is deleted, the index can be greater than the number of categories + if (position >= categories.size()) + return; + + Category category = categories.get(position); + List mangas = event.getMangas().get(category.id); + if (mangas == null) { + mangas = new ArrayList<>(); + } + setMangas(mangas); } protected void openManga(Manga manga) { @@ -115,7 +133,7 @@ public class LibraryCategoryFragment extends BaseFragment implements @Override public boolean onListItemClick(int position) { - if (actionMode != null && position != -1) { + if (getLibraryFragment().getActionMode() != null && position != -1) { toggleSelection(position); return true; } else { @@ -126,55 +144,29 @@ public class LibraryCategoryFragment extends BaseFragment implements @Override public void onListItemLongClick(int position) { - if (actionMode == null) - actionMode = ((BaseActivity) getActivity()).startSupportActionMode(this); - + getLibraryFragment().createActionModeIfNeeded(); toggleSelection(position); } private void toggleSelection(int position) { - adapter.toggleSelection(position, false); + LibraryFragment f = getLibraryFragment(); - int count = adapter.getSelectedItemCount(); + adapter.toggleSelection(position, false); + f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position)); + + int count = f.getPresenter().selectedMangas.size(); if (count == 0) { - actionMode.finish(); + f.destroyActionModeIfNeeded(); } else { - setContextTitle(count); - actionMode.invalidate(); + f.setContextTitle(count); + f.invalidateActionMode(); } } - private void setContextTitle(int count) { - actionMode.setTitle(getString(R.string.label_selected, count)); - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.getMenuInflater().inflate(R.menu.library_selection, menu); - adapter.setMode(LibraryCategoryAdapter.MODE_MULTI); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - adapter.setMode(LibraryCategoryAdapter.MODE_SINGLE); - adapter.clearSelection(); - actionMode = null; - } - - public void destroyActionModeIfNeeded() { - if (actionMode != null) { - actionMode.finish(); + public void setMode(int mode) { + adapter.setMode(mode); + if (mode == FlexibleAdapter.MODE_SINGLE) { + adapter.clearSelection(); } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryFragment.java b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryFragment.java index b7b27664f3..c0f55f46e0 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryFragment.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryFragment.java @@ -2,10 +2,12 @@ package eu.kanade.mangafeed.ui.library; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.design.widget.AppBarLayout; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.view.ViewPager; +import android.support.v7.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -13,21 +15,27 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import com.afollestad.materialdialogs.MaterialDialog; + import java.util.ArrayList; import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; +import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.database.models.Category; +import eu.kanade.mangafeed.data.database.models.Manga; import eu.kanade.mangafeed.data.sync.LibraryUpdateService; +import eu.kanade.mangafeed.ui.base.activity.BaseActivity; import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment; import eu.kanade.mangafeed.ui.library.category.CategoryFragment; import eu.kanade.mangafeed.ui.main.MainActivity; import nucleus.factory.RequiresPresenter; @RequiresPresenter(LibraryPresenter.class) -public class LibraryFragment extends BaseRxFragment { +public class LibraryFragment extends BaseRxFragment + implements ActionMode.Callback { TabLayout tabs; AppBarLayout appBar; @@ -35,6 +43,8 @@ public class LibraryFragment extends BaseRxFragment { @Bind(R.id.view_pager) ViewPager categoriesPager; protected LibraryAdapter adapter; + private ActionMode actionMode; + public static LibraryFragment newInstance() { return new LibraryFragment(); } @@ -83,7 +93,6 @@ public class LibraryFragment extends BaseRxFragment { Intent intent = LibraryUpdateService.getStartIntent(getActivity()); getActivity().startService(intent); } - return true; case R.id.action_edit_categories: onEditCategories(); @@ -112,4 +121,77 @@ public class LibraryFragment extends BaseRxFragment { tabs.setVisibility(actualCategories.size() == 1 ? View.GONE : View.VISIBLE); } + public void setContextTitle(int count) { + actionMode.setTitle(getString(R.string.label_selected, count)); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.library_selection, menu); + adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.action_move_to_category: + moveMangasToCategories(getPresenter().selectedMangas); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE); + getPresenter().selectedMangas.clear(); + actionMode = null; + } + + public void destroyActionModeIfNeeded() { + if (actionMode != null) { + actionMode.finish(); + } + } + + private void moveMangasToCategories(List mangas) { + new MaterialDialog.Builder(getActivity()) + .title(R.string.action_move_category) + .items(getPresenter().getCategoriesNames()) + .itemsCallbackMultiChoice(null, (dialog, which, text) -> { + getPresenter().moveMangasToCategories(which, mangas); + destroyActionModeIfNeeded(); + return true; + }) + .positiveText(R.string.button_ok) + .negativeText(R.string.button_cancel) + .show(); + + } + + @Nullable + public ActionMode getActionMode() { + return actionMode; + } + + public LibraryAdapter getAdapter() { + return adapter; + } + + public void createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = ((BaseActivity) getActivity()).startSupportActionMode(this); + } + } + + public void invalidateActionMode() { + actionMode.invalidate(); + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryPresenter.java index da132d0f99..03181be116 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/library/LibraryPresenter.java @@ -3,6 +3,7 @@ package eu.kanade.mangafeed.ui.library; import android.os.Bundle; import android.util.Pair; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -13,6 +14,7 @@ import eu.kanade.mangafeed.data.cache.CoverCache; import eu.kanade.mangafeed.data.database.DatabaseHelper; import eu.kanade.mangafeed.data.database.models.Category; import eu.kanade.mangafeed.data.database.models.Manga; +import eu.kanade.mangafeed.data.database.models.MangaCategory; import eu.kanade.mangafeed.data.preference.PreferencesHelper; import eu.kanade.mangafeed.data.source.SourceManager; import eu.kanade.mangafeed.event.LibraryMangasEvent; @@ -29,6 +31,7 @@ public class LibraryPresenter extends BasePresenter { @Inject SourceManager sourceManager; protected List categories; + protected List selectedMangas; private static final int GET_CATEGORIES = 1; @@ -36,6 +39,8 @@ public class LibraryPresenter extends BasePresenter { protected void onCreate(Bundle savedState) { super.onCreate(savedState); + selectedMangas = new ArrayList<>(); + restartableLatestCache(GET_CATEGORIES, this::getCategoriesObservable, LibraryFragment::onNextCategories); @@ -48,6 +53,12 @@ public class LibraryPresenter extends BasePresenter { } + @Override + protected void onDestroy() { + EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class); + super.onDestroy(); + } + public Observable> getCategoriesObservable() { return db.getCategories().createObservable() .doOnNext(categories -> this.categories = categories) @@ -72,5 +83,43 @@ public class LibraryPresenter extends BasePresenter { .subscribe()); } + public void setSelection(Manga manga, boolean selected) { + if (selected) { + selectedMangas.add(manga); + } else { + selectedMangas.remove(manga); + } + } + public String[] getCategoriesNames() { + int count = categories.size(); + String[] names = new String[count]; + + for (int i = 0; i < count; i++) { + names[i] = categories.get(i).name; + } + + return names; + } + + public void moveMangasToCategories(Integer[] positions, List mangas) { + List categoriesToAdd = new ArrayList<>(); + for (Integer index : positions) { + categoriesToAdd.add(categories.get(index)); + } + + moveMangasToCategories(categoriesToAdd, mangas); + } + + public void moveMangasToCategories(List categories, List mangas) { + List mc = new ArrayList<>(); + + for (Manga manga : mangas) { + for (Category cat : categories) { + mc.add(MangaCategory.create(manga, cat)); + } + } + + db.setMangaCategories(mc, mangas); + } } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/ViewPagerReaderAdapter.java b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/ViewPagerReaderAdapter.java index 52ced5b7a8..859a4e28f0 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/ViewPagerReaderAdapter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/common/ViewPagerReaderAdapter.java @@ -6,6 +6,7 @@ import android.support.v4.app.FragmentManager; import java.util.List; import eu.kanade.mangafeed.data.source.model.Page; +import eu.kanade.mangafeed.ui.base.adapter.SmartFragmentStatePagerAdapter; public class ViewPagerReaderAdapter extends SmartFragmentStatePagerAdapter { diff --git a/app/src/main/res/drawable-hdpi/ic_label.png b/app/src/main/res/drawable-hdpi/ic_label.png new file mode 100644 index 0000000000..5a3318e3c9 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_label.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_label.png b/app/src/main/res/drawable-ldpi/ic_label.png new file mode 100644 index 0000000000..11caa4c206 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_label.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_label.png b/app/src/main/res/drawable-mdpi/ic_label.png new file mode 100644 index 0000000000..7827c2925c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_label.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_label.png b/app/src/main/res/drawable-xhdpi/ic_label.png new file mode 100644 index 0000000000..e577059aeb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_label.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_label.png b/app/src/main/res/drawable-xxhdpi/ic_label.png new file mode 100644 index 0000000000..0ff6c9f297 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_label.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_label.png b/app/src/main/res/drawable-xxxhdpi/ic_label.png new file mode 100644 index 0000000000..2789b8cec8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_label.png differ diff --git a/app/src/main/res/menu/library_selection.xml b/app/src/main/res/menu/library_selection.xml index 24430edac4..8b10f09d07 100644 --- a/app/src/main/res/menu/library_selection.xml +++ b/app/src/main/res/menu/library_selection.xml @@ -1,12 +1,16 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + android:title="@string/action_delete" + android:icon="@drawable/ic_action_delete" + app:showAsAction="ifRoom"/> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62928794f6..c87e24bd20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Add category Edit categories Rename category + Move to categories Sort up Sort down Unread