From 59276b71609044bc7a748551ce334f3a159445b2 Mon Sep 17 00:00:00 2001 From: inorichi Date: Tue, 23 Feb 2016 14:51:18 +0100 Subject: [PATCH] Migrate library to Kotlin. --- .../data/library/LibraryUpdateService.kt | 8 +- .../tachiyomi/event/LibraryMangasEvent.java | 27 -- .../tachiyomi/event/LibraryMangasEvent.kt | 11 + .../tachiyomi/ui/category/CategoryActivity.kt | 22 +- .../tachiyomi/ui/library/LibraryAdapter.java | 47 --- .../tachiyomi/ui/library/LibraryAdapter.kt | 79 ++++ .../ui/library/LibraryCategoryAdapter.java | 75 ---- .../ui/library/LibraryCategoryAdapter.kt | 112 ++++++ .../ui/library/LibraryCategoryFragment.java | 201 ---------- .../ui/library/LibraryCategoryFragment.kt | 263 ++++++++++++ .../tachiyomi/ui/library/LibraryFragment.java | 342 ---------------- .../tachiyomi/ui/library/LibraryFragment.kt | 377 ++++++++++++++++++ .../tachiyomi/ui/library/LibraryHolder.java | 55 --- .../tachiyomi/ui/library/LibraryHolder.kt | 58 +++ .../ui/library/LibraryPresenter.java | 157 -------- .../tachiyomi/ui/library/LibraryPresenter.kt | 227 +++++++++++ .../tachiyomi/util/ContextExtensions.kt | 7 + .../tachiyomi/util/ViewGroupExtensions.kt | 2 +- .../res/layout/fragment_library_category.xml | 2 +- .../library/LibraryUpdateServiceTest.java | 2 +- 20 files changed, 1149 insertions(+), 925 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index fa71ca68c4..c5a4a22d0e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.library -import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver @@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.AndroidComponentUtil import eu.kanade.tachiyomi.util.NetworkUtil import eu.kanade.tachiyomi.util.notification +import eu.kanade.tachiyomi.util.notificationManager import rx.Observable import rx.Subscription import rx.schedulers.Schedulers @@ -310,12 +310,6 @@ class LibraryUpdateService : Service() { notificationManager.cancel(UPDATE_NOTIFICATION_ID) } - /** - * Property that returns the notification manager. - */ - private val notificationManager : NotificationManager - get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - /** * Property that returns an intent to open the main activity. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.java b/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.java deleted file mode 100644 index 1250a0d4b6..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package eu.kanade.tachiyomi.event; - -import android.support.annotation.Nullable; - -import java.util.List; -import java.util.Map; - -import eu.kanade.tachiyomi.data.database.models.Category; -import eu.kanade.tachiyomi.data.database.models.Manga; - -public class LibraryMangasEvent { - - private final Map> mangas; - - public LibraryMangasEvent(Map> mangas) { - this.mangas = mangas; - } - - public Map> getMangas() { - return mangas; - } - - @Nullable - public List getMangasForCategory(Category category) { - return mangas.get(category.id); - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.kt b/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.kt new file mode 100644 index 0000000000..8dfd585f92 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/event/LibraryMangasEvent.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.event + +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga + +class LibraryMangasEvent(val mangas: Map>) { + + fun getMangasForCategory(category: Category): List? { + return mangas[category.id] + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt index 346873539e..71a0fb6066 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryActivity.kt @@ -10,12 +10,12 @@ import android.support.v7.widget.helper.ItemTouchHelper import android.view.Menu import android.view.MenuItem import com.afollestad.materialdialogs.MaterialDialog +import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener -import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import kotlinx.android.synthetic.main.activity_edit_categories.* import kotlinx.android.synthetic.main.toolbar.* import nucleus.factory.RequiresPresenter @@ -93,7 +93,7 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac * Call this when action mode action is finished. */ fun destroyActionModeIfNeeded() { - actionMode?.finish() + actionMode?.finish() } /** @@ -163,8 +163,8 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac it.invalidate() // Show edit button only when one item is selected - val editItem = it.menu?.findItem(R.id.action_edit) - editItem?.isVisible = count == 1 + val editItem = it.menu.findItem(R.id.action_edit) + editItem.isVisible = count == 1 } } } @@ -192,15 +192,14 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac R.id.action_delete -> { // Delete select categories. deleteCategories(getSelectedCategories()) - return true } R.id.action_edit -> { // Edit selected category editCategory(getSelectedCategories()?.get(0)) - return true } + else -> return false } - return false + return true } /** @@ -215,7 +214,7 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac // Inflate menu. mode.menuInflater.inflate(R.menu.category_selection, menu) // Enable adapter multi selection. - adapter.mode = LibraryCategoryAdapter.MODE_MULTI + adapter.mode = FlexibleAdapter.MODE_MULTI return true } @@ -226,7 +225,7 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac */ override fun onDestroyActionMode(mode: ActionMode?) { // Reset adapter to single selection - adapter.mode = LibraryCategoryAdapter.MODE_SINGLE + adapter.mode = FlexibleAdapter.MODE_SINGLE // Clear selected items adapter.clearSelection() actionMode = null @@ -239,8 +238,9 @@ class CategoryActivity : BaseRxActivity(), ActionMode.Callbac */ override fun onListItemClick(position: Int): Boolean { // Check if action mode is initialized and selected item exist. - if (actionMode != null && position != -1) { - // Toggle selection of clicked item. + if (position == -1) { + return false + } else if (actionMode != null) { toggleSelection(position) return true } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.java deleted file mode 100644 index bb9b3e858b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; - -import java.util.List; - -import eu.kanade.tachiyomi.data.database.models.Category; -import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter; - -public class LibraryAdapter extends SmartFragmentStatePagerAdapter { - - protected List categories; - - public LibraryAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public Fragment getItem(int position) { - return LibraryCategoryFragment.newInstance(position); - } - - @Override - public int getCount() { - return categories == null ? 0 : categories.size(); - } - - @Override - public CharSequence getPageTitle(int position) { - return categories.get(position).name; - } - - public void setCategories(List categories) { - if (this.categories != categories) { - this.categories = categories; - 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/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt new file mode 100644 index 0000000000..258f65a616 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.ui.library + +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager + +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter + +/** + * This adapter stores the categories from the library, used with a ViewPager. + * + * @param fm the fragment manager. + * @constructor creates an instance of the adapter. + */ +class LibraryAdapter(fm: FragmentManager) : SmartFragmentStatePagerAdapter(fm) { + + /** + * The categories to bind in the adapter. + */ + var categories: List? = null + // This setter helps to not refresh the adapter if the reference to the list doesn't change. + set(value) { + if (field !== value) { + field = value + notifyDataSetChanged() + } + } + + /** + * Creates a new fragment for the given position when it's called. + * + * @param position the position to instantiate. + * @return a fragment for the given position. + */ + override fun getItem(position: Int): Fragment { + return LibraryCategoryFragment.newInstance(position) + } + + /** + * Returns the number of categories. + * + * @return the number of categories or 0 if the list is null. + */ + override fun getCount(): Int { + return categories?.size ?: 0 + } + + /** + * Returns the title to display for a category. + * + * @param position the position of the element. + * @return the title to display. + */ + override fun getPageTitle(position: Int): CharSequence { + return categories!![position].name + } + + /** + * Method to enable or disable the action mode (multiple selection) for all the instantiated + * fragments. + * + * @param mode the mode to set. + */ + fun setSelectionMode(mode: Int) { + for (fragment in registeredFragments) { + (fragment as LibraryCategoryFragment).setSelectionMode(mode) + } + } + + /** + * Notifies the adapters in all the registered fragments to refresh their content. + */ + fun refreshRegisteredAdapters() { + for (fragment in registeredFragments) { + (fragment as LibraryCategoryFragment).adapter.notifyDataSetChanged() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.java deleted file mode 100644 index 79d77a2ef0..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import java.util.ArrayList; -import java.util.List; - -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.kanade.tachiyomi.R; -import eu.kanade.tachiyomi.data.database.models.Manga; - -public class LibraryCategoryAdapter extends FlexibleAdapter { - - private List mangas; - private LibraryCategoryFragment fragment; - - public LibraryCategoryAdapter(LibraryCategoryFragment fragment) { - this.fragment = fragment; - mItems = new ArrayList<>(); - setHasStableIds(true); - } - - public void setItems(List list) { - mItems = list; - - // A copy of manga that it's always unfiltered - mangas = new ArrayList<>(list); - updateDataSet(null); - } - - public void clear() { - mItems.clear(); - } - - @Override - public long getItemId(int position) { - return mItems.get(position).id; - } - - @Override - public void updateDataSet(String param) { - if (mangas != null) { - filterItems(mangas); - notifyDataSetChanged(); - } - } - - @Override - protected boolean filterObject(Manga manga, String query) { - return (manga.title != null && manga.title.toLowerCase().contains(query)) || - (manga.author != null && manga.author.toLowerCase().contains(query)); - } - - @Override - public LibraryHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_catalogue_grid, parent, false); - return new LibraryHolder(v, this, fragment); - } - - @Override - public void onBindViewHolder(LibraryHolder holder, int position) { - final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter(); - final Manga manga = getItem(position); - holder.onSetValues(manga, presenter); - //When user scrolls this bind the correct selection status - holder.itemView.setActivated(isSelected(position)); - } - - public int getCoverHeight() { - return fragment.recycler.getItemWidth() / 3 * 4; - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt new file mode 100644 index 0000000000..6d99d6971d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -0,0 +1,112 @@ +package eu.kanade.tachiyomi.ui.library + +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.RelativeLayout +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.fragment_library_category.* +import kotlinx.android.synthetic.main.item_catalogue_grid.view.* +import java.util.* + +/** + * Adapter storing a list of manga in a certain category. + * + * @param fragment the fragment containing this adapter. + */ +class LibraryCategoryAdapter(private val fragment: LibraryCategoryFragment) : + FlexibleAdapter() { + + /** + * The list of manga in this category. + */ + private var mangas: List? = null + + init { + setHasStableIds(true) + } + + /** + * Sets a list of manga in the adapter. + * + * @param list the list to set. + */ + fun setItems(list: List) { + mItems = list + + // A copy of manga that it's always unfiltered + mangas = ArrayList(list) + updateDataSet(null) + } + + /** + * Returns the identifier for a manga. + * + * @param position the position in the adapter. + * @return an identifier for the item. + */ + override fun getItemId(position: Int): Long { + return mItems[position].id + } + + /** + * Filters the list of manga applying [filterObject] for each element. + * + * @param param the filter. Not used. + */ + override fun updateDataSet(param: String?) { + mangas?.let { + filterItems(it) + 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 != null && 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 { + val view = parent.inflate(R.layout.item_catalogue_grid) + view.image_container.layoutParams = RelativeLayout.LayoutParams(MATCH_PARENT, coverHeight) + return LibraryHolder(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 presenter = (fragment.parentFragment as LibraryFragment).presenter + val manga = getItem(position) + + holder.onSetValues(manga, presenter) + //When user scrolls this bind the correct selection status + holder.itemView.isActivated = isSelected(position) + } + + /** + * Property to return the height for the covers based on the width to keep an aspect ratio. + */ + val coverHeight: Int + get() = fragment.recycler.itemWidth / 3 * 4 + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.java deleted file mode 100644 index 40cb63b876..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.java +++ /dev/null @@ -1,201 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.f2prateek.rx.preferences.Preference; - -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.kanade.tachiyomi.R; -import eu.kanade.tachiyomi.data.database.models.Category; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.event.LibraryMangasEvent; -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; -import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment; -import eu.kanade.tachiyomi.ui.manga.MangaActivity; -import eu.kanade.tachiyomi.widget.AutofitRecyclerView; -import icepick.State; -import rx.Subscription; - -public class LibraryCategoryFragment extends BaseFragment - implements FlexibleViewHolder.OnListItemClickListener { - - @Bind(R.id.library_mangas) AutofitRecyclerView recycler; - - @State int position; - private LibraryCategoryAdapter adapter; - private List mangas; - - private Subscription numColumnsSubscription; - private Subscription searchSubscription; - - public static LibraryCategoryFragment newInstance(int position) { - LibraryCategoryFragment fragment = new LibraryCategoryFragment(); - fragment.position = position; - return fragment; - } - - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { - // Inflate the layout for this fragment - View view = inflater.inflate(R.layout.fragment_library_category, container, false); - ButterKnife.bind(this, view); - - 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() : - getLibraryPresenter().preferences.landscapeColumns(); - - numColumnsSubscription = columnsPref.asObservable() - .doOnNext(recycler::setSpanCount) - .skip(1) - // Set again the adapter to recalculate the covers height - .subscribe(count -> recycler.setAdapter(adapter)); - - if (savedState != null) { - adapter.onRestoreInstanceState(savedState); - - if (adapter.getMode() == FlexibleAdapter.MODE_SINGLE) { - adapter.clearSelection(); - } - } - - searchSubscription = getLibraryPresenter().searchSubject - .subscribe(text -> { - adapter.setSearchText(text); - adapter.updateDataSet(); - }); - - - - return view; - - } - - @Override - public void onDestroyView() { - numColumnsSubscription.unsubscribe(); - searchSubscription.unsubscribe(); - super.onDestroyView(); - } - - @Override - public void onResume() { - super.onResume(); - registerForEvents(); - } - - @Override - public void onPause() { - unregisterForEvents(); - super.onPause(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - adapter.onSaveInstanceState(outState); - super.onSaveInstanceState(outState); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(LibraryMangasEvent event) { - 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.getMangasForCategory(category); - if (this.mangas != mangas) { - this.mangas = mangas; - if (mangas == null) { - mangas = new ArrayList<>(); - } - setMangas(mangas); - } - } - - protected void openManga(Manga manga) { - getLibraryPresenter().onOpenManga(manga); - Intent intent = MangaActivity.newIntent(getActivity(), manga); - startActivity(intent); - } - - public void setMangas(List mangas) { - if (mangas != null) { - adapter.setItems(mangas); - } else { - adapter.clear(); - } - } - - @Override - public boolean onListItemClick(int position) { - if (getLibraryFragment().getActionMode() != null && position != -1) { - toggleSelection(position); - return true; - } else { - openManga(adapter.getItem(position)); - return false; - } - } - - @Override - public void onListItemLongClick(int position) { - getLibraryFragment().createActionModeIfNeeded(); - toggleSelection(position); - } - - private void toggleSelection(int position) { - LibraryFragment f = getLibraryFragment(); - adapter.toggleSelection(position, false); - f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position)); - - int count = f.getPresenter().selectedMangas.size(); - if (count == 0) { - f.destroyActionModeIfNeeded(); - } - else { - f.setContextTitle(count); - f.setVisibilityOfCoverEdit(count); - f.invalidateActionMode(); - } - } - - public void setMode(int mode) { - adapter.setMode(mode); - if (mode == FlexibleAdapter.MODE_SINGLE) { - adapter.clearSelection(); - } - } - - private LibraryFragment getLibraryFragment() { - return (LibraryFragment) getParentFragment(); - } - - private LibraryPresenter getLibraryPresenter() { - return getLibraryFragment().getPresenter(); - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt new file mode 100644 index 0000000000..2ed052b6cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryFragment.kt @@ -0,0 +1,263 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.f2prateek.rx.preferences.Preference +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.event.LibraryMangasEvent +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment +import eu.kanade.tachiyomi.ui.manga.MangaActivity +import kotlinx.android.synthetic.main.fragment_library_category.* +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Subscription +import java.util.* + +/** + * Fragment containing the library manga for a certain category. + * Uses R.layout.fragment_library_category. + */ +class LibraryCategoryFragment : BaseFragment(), FlexibleViewHolder.OnListItemClickListener { + + /** + * Adapter to hold the manga in this category. + */ + lateinit var adapter: LibraryCategoryAdapter + private set + + /** + * Position in the adapter from [LibraryAdapter]. + */ + private var position: Int = 0 + + /** + * Manga in this category. + */ + private var mangas: List? = null + set(value) { + field = value ?: ArrayList() + } + + /** + * Subscription of the number of manga per row. + */ + private var numColumnsSubscription: Subscription? = null + + /** + * Subscription of the library search. + */ + private var searchSubscription: Subscription? = null + + companion object { + /** + * Key to save and restore [position] from a [Bundle]. + */ + const val POSITION_KEY = "position_key" + + /** + * Creates a new instance of this class. + * + * @param position the position in the adapter from [LibraryAdapter]. + * @return a new instance of [LibraryCategoryFragment]. + */ + fun newInstance(position: Int): LibraryCategoryFragment { + val fragment = LibraryCategoryFragment() + fragment.position = position + return fragment + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_library_category, container, false) + } + + override fun onViewCreated(view: View, savedState: Bundle?) { + adapter = LibraryCategoryAdapter(this) + recycler.setHasFixedSize(true) + recycler.adapter = adapter + + if (libraryFragment.actionMode != null) { + setSelectionMode(FlexibleAdapter.MODE_MULTI) + } + + numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() + .doOnNext { recycler.spanCount = it } + .skip(1) + // Set again the adapter to recalculate the covers height + .subscribe { recycler.adapter = adapter } + + searchSubscription = libraryPresenter.searchSubject.subscribe { text -> + adapter.searchText = text + adapter.updateDataSet() + } + + if (savedState != null) { + position = savedState.getInt(POSITION_KEY) + adapter.onRestoreInstanceState(savedState) + + if (adapter.mode == FlexibleAdapter.MODE_SINGLE) { + adapter.clearSelection() + } + } + } + + override fun onDestroyView() { + numColumnsSubscription?.unsubscribe() + searchSubscription?.unsubscribe() + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + registerForEvents() + } + + override fun onPause() { + unregisterForEvents() + super.onPause() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(POSITION_KEY, position) + adapter.onSaveInstanceState(outState) + super.onSaveInstanceState(outState) + } + + /** + * Subscribe to [LibraryMangasEvent]. When an event is received, it updates [mangas] if needed + * and refresh the content of the adapter. + * + * @param event the event received. + */ + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: LibraryMangasEvent) { + // Get the categories from the parent fragment. + val categories = libraryFragment.adapter.categories ?: return + + // When a category is deleted, the index can be greater than the number of categories. + if (position >= categories.size) return + + // Get the manga list for this category + val mangaForCategory = event.getMangasForCategory(categories[position]) + + // Update the list only if the reference to the list is different, avoiding reseting the + // adapter after every onResume. + if (mangas !== mangaForCategory) { + mangas = mangaForCategory + mangas?.let { adapter.setItems(it) } + } + } + + /** + * Called when a manga is clicked. + * + * @param position the position of the element clicked. + * @return true if the item should be selected, false otherwise. + */ + override fun onListItemClick(position: Int): Boolean { + // If the action mode is created and the position is valid, toggle the selection. + if (position == -1) { + return false + } else if (libraryFragment.actionMode != null) { + toggleSelection(position) + return true + } else { + openManga(adapter.getItem(position)) + return false + } + } + + /** + * Called when a manga is long clicked. + * + * @param position the position of the element clicked. + */ + override fun onListItemLongClick(position: Int) { + libraryFragment.createActionModeIfNeeded() + toggleSelection(position) + } + + /** + * Opens a manga. + * + * @param manga the manga to open. + */ + protected fun openManga(manga: Manga) { + // Notify the presenter a manga is being opened. + libraryPresenter.onOpenManga() + + // Create a new activity with the manga. + val intent = MangaActivity.newIntent(activity, manga) + startActivity(intent) + } + + /** + * Toggles the selection for a manga. + * + * @param position the position to toggle. + */ + private fun toggleSelection(position: Int) { + val library = libraryFragment + + // Toggle the selection. + adapter.toggleSelection(position, false) + + // Notify the selection to the presenter. + library.presenter.setSelection(adapter.getItem(position), adapter.isSelected(position)) + + // Get the selected count. + val count = library.presenter.selectedMangas.size + if (count == 0) { + // Destroy action mode if there are no items selected. + library.destroyActionModeIfNeeded() + } else { + // Update action mode with the new selection. + library.setContextTitle(count) + library.setVisibilityOfCoverEdit(count) + library.invalidateActionMode() + } + } + + /** + * Returns a preference for the number of manga per row based on the current orientation. + * + * @return the preference. + */ + fun getColumnsPreferenceForCurrentOrientation(): Preference { + return if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) + libraryPresenter.preferences.portraitColumns() + else + libraryPresenter.preferences.landscapeColumns() + } + + /** + * Sets the mode for the adapter. + * + * @param mode the mode to set. It should be MODE_SINGLE or MODE_MULTI. + */ + fun setSelectionMode(mode: Int) { + adapter.mode = mode + if (mode == FlexibleAdapter.MODE_SINGLE) { + adapter.clearSelection() + } + } + + /** + * Property to get the library fragment. + */ + private val libraryFragment: LibraryFragment + get() = parentFragment as LibraryFragment + + /** + * Property to get the library presenter. + */ + private val libraryPresenter: LibraryPresenter + get() = libraryFragment.presenter + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java deleted file mode 100644 index e397f2045d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.java +++ /dev/null @@ -1,342 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -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.view.ViewPager; -import android.support.v7.view.ActionMode; -import android.support.v7.widget.SearchView; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import com.afollestad.materialdialogs.MaterialDialog; - -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import butterknife.Bind; -import butterknife.ButterKnife; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.kanade.tachiyomi.R; -import eu.kanade.tachiyomi.data.database.models.Category; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.data.io.IOHandler; -import eu.kanade.tachiyomi.data.library.LibraryUpdateService; -import eu.kanade.tachiyomi.event.LibraryMangasEvent; -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.ToastUtil; -import icepick.State; -import nucleus.factory.RequiresPresenter; - -@RequiresPresenter(LibraryPresenter.class) -public class LibraryFragment extends BaseRxFragment - implements ActionMode.Callback { - - - private static final int REQUEST_IMAGE_OPEN = 101; - - protected LibraryAdapter adapter; - - @Bind(R.id.view_pager) ViewPager viewPager; - - @State int activeCategory; - - @State String query = ""; - - private TabLayout tabs; - - private AppBarLayout appBar; - - private ActionMode actionMode; - - private Manga selectedCoverManga; - - public static LibraryFragment newInstance() { - return new LibraryFragment(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { - // Inflate the layout for this fragment - View view = inflater.inflate(R.layout.fragment_library, container, false); - setToolbarTitle(getString(R.string.label_library)); - ButterKnife.bind(this, view); - - appBar = ((MainActivity) getActivity()).getAppBar(); - tabs = (TabLayout) inflater.inflate(R.layout.library_tab_layout, appBar, false); - appBar.addView(tabs); - - adapter = new LibraryAdapter(getChildFragmentManager()); - viewPager.setAdapter(adapter); - tabs.setupWithViewPager(viewPager); - - if (savedState != null) { - getPresenter().searchSubject.onNext(query); - } - - return view; - } - - @Override - public void onDestroyView() { - appBar.removeView(tabs); - super.onDestroyView(); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - activeCategory = viewPager.getCurrentItem(); - super.onSaveInstanceState(bundle); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.library, menu); - - // Initialize search menu - MenuItem searchItem = menu.findItem(R.id.action_search); - final SearchView searchView = (SearchView) searchItem.getActionView(); - - if (!TextUtils.isEmpty(query)) { - searchItem.expandActionView(); - searchView.setQuery(query, true); - searchView.clearFocus(); - } - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - onSearchTextChange(query); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - onSearchTextChange(newText); - return true; - } - }); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_refresh: - LibraryUpdateService.start(getActivity()); - return true; - case R.id.action_edit_categories: - onEditCategories(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - private void onSearchTextChange(String query) { - this.query = query; - getPresenter().searchSubject.onNext(query); - } - - private void onEditCategories() { - Intent intent = CategoryActivity.newIntent(getActivity()); - startActivity(intent); - } - - public void onNextLibraryUpdate(List categories, Map> mangas) { - boolean hasMangasInDefaultCategory = mangas.get(0) != null; - int activeCat = adapter.categories != null ? viewPager.getCurrentItem() : activeCategory; - - if (hasMangasInDefaultCategory) { - setCategoriesWithDefault(categories); - } else { - setCategories(categories); - } - // Restore active category - viewPager.setCurrentItem(activeCat, false); - if (tabs.getTabCount() > 0) { - TabLayout.Tab tab = tabs.getTabAt(viewPager.getCurrentItem()); - if (tab != null) tab.select(); - } - - // Send the mangas to child fragments after the adapter is updated - EventBus.getDefault().postSticky(new LibraryMangasEvent(mangas)); - } - - private void setCategoriesWithDefault(List categories) { - List categoriesWithDefault = new ArrayList<>(); - categoriesWithDefault.add(Category.createDefault()); - categoriesWithDefault.addAll(categories); - - setCategories(categoriesWithDefault); - } - - private void setCategories(List categories) { - adapter.setCategories(categories); - tabs.setTabsFromPagerAdapter(adapter); - tabs.setVisibility(categories.size() <= 1 ? View.GONE : View.VISIBLE); - } - - public void setContextTitle(int count) { - actionMode.setTitle(getString(R.string.label_selected, count)); - } - - public void setVisibilityOfCoverEdit(int count) { - // If count = 1 display edit button - actionMode.getMenu().findItem(R.id.action_edit_cover).setVisible((count == 1)); - } - - @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_edit_cover: - changeSelectedCover(getPresenter().selectedMangas); - rebuildAdapter(); - destroyActionModeIfNeeded(); - return true; - case R.id.action_move_to_category: - moveMangasToCategories(getPresenter().selectedMangas); - return true; - case R.id.action_delete: - getPresenter().deleteMangas(); - destroyActionModeIfNeeded(); - return true; - } - return false; - } - - /** - * TODO workaround. Covers won't refresh any other way. - */ - public void rebuildAdapter() { - adapter = new LibraryAdapter(getChildFragmentManager()); - viewPager.setAdapter(adapter); - tabs.setupWithViewPager(viewPager); - } - - @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 changeSelectedCover(List mangas) { - if (mangas.size() == 1) { - selectedCoverManga = mangas.get(0); - if (selectedCoverManga.favorite) { - - Intent intent = new Intent(); - intent.setType("image/*"); - intent.setAction(Intent.ACTION_GET_CONTENT); - startActivityForResult(Intent.createChooser(intent, - getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN); - } else { - ToastUtil.showShort(getContext(), R.string.notification_first_add_to_library); - } - - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK) { - switch (requestCode) { - case (REQUEST_IMAGE_OPEN): - if (selectedCoverManga != null) { - // Get the file's content URI from the incoming Intent - Uri selectedImageUri = data.getData(); - - // Convert to absolute path to prevent FileNotFoundException - String result = IOHandler.getFilePath(selectedImageUri, - getContext().getContentResolver(), getContext()); - - // Get file from filepath - File picture = new File(result != null ? result : ""); - - try { - // Update cover to selected file, show error if something went wrong - if (!getPresenter().editCoverWithLocalFile(picture, selectedCoverManga)) - ToastUtil.showShort(getContext(), R.string.notification_manga_update_failed); - - } catch (IOException e) { - e.printStackTrace(); - } - } - break; - } - } - } - - 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 = getBaseActivity().startSupportActionMode(this); - } - } - - public void invalidateActionMode() { - actionMode.invalidate(); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt new file mode 100644 index 0000000000..6b5166cce7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -0,0 +1,377 @@ +package eu.kanade.tachiyomi.ui.library + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.AppBarLayout +import android.support.design.widget.TabLayout +import android.support.v7.view.ActionMode +import android.support.v7.widget.SearchView +import android.view.* +import butterknife.ButterKnife +import com.afollestad.materialdialogs.MaterialDialog +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.io.IOHandler +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.event.LibraryMangasEvent +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.ToastUtil +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.fragment_library.* +import nucleus.factory.RequiresPresenter +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.IOException + +/** + * Fragment that shows the manga from the library. + * Uses R.layout.fragment_library. + */ +@RequiresPresenter(LibraryPresenter::class) +class LibraryFragment : BaseRxFragment(), ActionMode.Callback { + + /** + * Adapter containing the categories of the library. + */ + lateinit var adapter: LibraryAdapter + private set + + /** + * TabLayout of the categories. + */ + private lateinit var tabs: TabLayout + + /** + * AppBarLayout from [MainActivity]. + */ + private lateinit var appBar: AppBarLayout + + /** + * 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. + */ + var actionMode: ActionMode? = null + private set + + /** + * Selected manga for editing its cover. + */ + private var selectedCoverManga: Manga? = null + + companion object { + /** + * Key to change the cover of a manga in [onActivityResult]. + */ + const val REQUEST_IMAGE_OPEN = 101 + + /** + * Key to add a manga to an [Intent]. + */ + const val MANGA_EXTRA = "manga_extra" + + /** + * 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]. + */ + @JvmStatic + fun newInstance(): LibraryFragment { + return LibraryFragment() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + 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)) + ButterKnife.bind(this, view) + + appBar = (activity as MainActivity).appBar + tabs = appBar.inflate(R.layout.library_tab_layout) as TabLayout + appBar.addView(tabs) + + adapter = LibraryAdapter(childFragmentManager) + view_pager.adapter = adapter + tabs.setupWithViewPager(view_pager) + + if (savedState != null) { + activeCategory = savedState.getInt(CATEGORY_KEY) + query = savedState.getString(QUERY_KEY) + presenter.searchSubject.onNext(query) + } + } + + override fun onDestroyView() { + appBar.removeView(tabs) + super.onDestroyView() + } + + override fun onSaveInstanceState(bundle: Bundle) { + bundle.putInt(CATEGORY_KEY, view_pager.currentItem) + bundle.putString(QUERY_KEY, query) + super.onSaveInstanceState(bundle) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.library, menu) + + // Initialize search 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() + } + + 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 onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_refresh -> LibraryUpdateService.start(activity) + R.id.action_edit_categories -> { + val intent = CategoryActivity.newIntent(activity) + startActivity(intent) + } + else -> return super.onOptionsItemSelected(item) + } + + return true + } + + /** + * 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. + presenter.searchSubject.onNext(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, mangaMap: Map>) { + // Get the current active category. + val activeCat = if (adapter.categories != null) view_pager.currentItem else activeCategory + + // Add the default category if it contains manga. + if (mangaMap[0] != null) { + setCategories(arrayListOf(Category.createDefault()) + categories) + } else { + setCategories(categories) + } + + // Restore active category. + view_pager.setCurrentItem(activeCat, false) + if (tabs.tabCount > 0) { + tabs.getTabAt(view_pager.currentItem)?.select() + } + + // Send the manga map to child fragments after the adapter is updated. + EventBus.getDefault().postSticky(LibraryMangasEvent(mangaMap)) + } + + /** + * Sets the categories in the adapter and the tab layout. + * + * @param categories the categories to set. + */ + private fun setCategories(categories: List) { + adapter.categories = categories + tabs.setTabsFromPagerAdapter(adapter) + tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE + } + + /** + * Sets the title of the action mode. + * + * @param count the number of items selected. + */ + fun setContextTitle(count: Int) { + actionMode?.title = getString(R.string.label_selected, count) + } + + /** + * Sets the visibility of the edit cover item. + * + * @param count the number of items selected. + */ + fun setVisibilityOfCoverEdit(count: Int) { + // If count = 1 display edit button + actionMode?.menu?.findItem(R.id.action_edit_cover)?.isVisible = count == 1 + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.library_selection, menu) + adapter.setSelectionMode(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_edit_cover -> { + changeSelectedCover(presenter.selectedMangas) + adapter.refreshRegisteredAdapters() + destroyActionModeIfNeeded() + } + R.id.action_move_to_category -> { + moveMangasToCategories(presenter.selectedMangas) + } + R.id.action_delete -> { + presenter.deleteMangas() + destroyActionModeIfNeeded() + } + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE) + presenter.selectedMangas.clear() + actionMode = null + } + + /** + * Destroys the action mode. + */ + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + /** + * Changes the cover for the selected manga. + * + * @param mangas a list of selected manga. + */ + private fun changeSelectedCover(mangas: List) { + 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 { + ToastUtil.showShort(context, R.string.notification_first_add_to_library) + } + + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMAGE_OPEN) { + selectedCoverManga?.let { manga -> + // Get the file's content URI from the incoming Intent + val selectedImageUri = data.data + + // Convert to absolute path to prevent FileNotFoundException + val result = IOHandler.getFilePath(selectedImageUri, + context.contentResolver, context) + + // Get file from filepath + val picture = File(result ?: "") + + try { + // Update cover to selected file, show error if something went wrong + if (!presenter.editCoverWithLocalFile(picture, manga)) + ToastUtil.showShort(context, R.string.notification_manga_update_failed) + + } catch (e: IOException) { + e.printStackTrace() + } + } + + } + } + + /** + * Move the selected manga to a list of categories. + * + * @param mangas the manga list to move. + */ + private fun moveMangasToCategories(mangas: List) { + MaterialDialog.Builder(activity) + .title(R.string.action_move_category) + .items(presenter.getCategoryNames()) + .itemsCallbackMultiChoice(null) { dialog, positions, text -> + presenter.moveMangasToCategories(positions, mangas) + destroyActionModeIfNeeded() + true + } + .positiveText(R.string.button_ok) + .negativeText(R.string.button_cancel) + .show() + } + + /** + * Creates the action mode if it's not created already. + */ + fun createActionModeIfNeeded() { + if (actionMode == null) { + actionMode = baseActivity.startSupportActionMode(this) + } + } + + /** + * Invalidates the action mode, forcing it to refresh its content. + */ + fun invalidateActionMode() { + actionMode?.invalidate() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.java deleted file mode 100644 index ddc104a012..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import butterknife.Bind; -import butterknife.ButterKnife; -import eu.kanade.tachiyomi.R; -import eu.kanade.tachiyomi.data.cache.CoverCache; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.data.source.base.Source; -import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; - -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.widget.RelativeLayout.LayoutParams; - -public class LibraryHolder extends FlexibleViewHolder { - - @Bind(R.id.image_container) FrameLayout container; - @Bind(R.id.thumbnail) ImageView thumbnail; - @Bind(R.id.title) TextView title; - @Bind(R.id.unreadText) TextView unreadText; - - public LibraryHolder(View view, LibraryCategoryAdapter adapter, OnListItemClickListener listener) { - super(view, adapter, listener); - ButterKnife.bind(this, view); - container.setLayoutParams(new LayoutParams(MATCH_PARENT, adapter.getCoverHeight())); - } - - public void onSetValues(Manga manga, LibraryPresenter presenter) { - title.setText(manga.title); - - if (manga.unread > 0) { - unreadText.setVisibility(View.VISIBLE); - unreadText.setText(Integer.toString(manga.unread)); - } else { - unreadText.setVisibility(View.GONE); - } - - loadCover(manga, presenter.sourceManager.get(manga.source), presenter.coverCache); - } - - private void loadCover(Manga manga, Source source, CoverCache coverCache) { - if (manga.thumbnail_url != null) { - coverCache.saveOrLoadFromCache(thumbnail, manga.thumbnail_url, source.getGlideHeaders()); - } else { - thumbnail.setImageResource(android.R.color.transparent); - } - } - - - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt new file mode 100644 index 0000000000..1b59864693 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.ui.library + +import android.view.View +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.source.base.Source +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import kotlinx.android.synthetic.main.item_catalogue_grid.view.* + +/** + * Class used to hold the displayed data of a manga in the library, like the cover or the title. + * All the elements from the layout file "item_catalogue_grid" are available in this class. + * + * @param view the inflated view for this holder. + * @param adapter the adapter handling this holder. + * @param listener a listener to react to single tap and long tap events. + * @constructor creates a new library holder. + */ +class LibraryHolder(view: View, adapter: LibraryCategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener) : + FlexibleViewHolder(view, adapter, listener) { + + /** + * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this + * holder with the given manga. + * + * @param manga the manga to bind. + * @param presenter the library presenter. + */ + fun onSetValues(manga: Manga, presenter: LibraryPresenter) { + // Update the title of the manga. + itemView.title.text = manga.title + + // Update the unread count and its visibility. + with(itemView.unreadText) { + visibility = if (manga.unread > 0) View.VISIBLE else View.GONE + text = manga.unread.toString() + } + + // Update the cover. + loadCover(manga, presenter.sourceManager.get(manga.source)!!, presenter.coverCache) + } + + /** + * Load the cover of a manga in a image view. + * + * @param manga the manga to bind. + * @param source the source of the manga. + * @param coverCache the cache that stores the cover in the filesystem. + */ + private fun loadCover(manga: Manga, source: Source, coverCache: CoverCache) { + if (manga.thumbnail_url != null) { + coverCache.saveOrLoadFromCache(itemView.thumbnail, manga.thumbnail_url, source.glideHeaders) + } else { + itemView.thumbnail.setImageResource(android.R.color.transparent) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.java deleted file mode 100644 index 3d51127e52..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.java +++ /dev/null @@ -1,157 +0,0 @@ -package eu.kanade.tachiyomi.ui.library; - -import android.os.Bundle; -import android.util.Pair; - -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; - -import eu.kanade.tachiyomi.data.cache.CoverCache; -import eu.kanade.tachiyomi.data.database.DatabaseHelper; -import eu.kanade.tachiyomi.data.database.models.Category; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.data.database.models.MangaCategory; -import eu.kanade.tachiyomi.data.preference.PreferencesHelper; -import eu.kanade.tachiyomi.data.source.SourceManager; -import eu.kanade.tachiyomi.event.LibraryMangasEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.subjects.BehaviorSubject; - -public class LibraryPresenter extends BasePresenter { - - private static final int GET_LIBRARY = 1; - protected List categories; - protected List selectedMangas; - protected BehaviorSubject searchSubject; - @Inject DatabaseHelper db; - @Inject PreferencesHelper preferences; - @Inject CoverCache coverCache; - @Inject SourceManager sourceManager; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - selectedMangas = new ArrayList<>(); - - searchSubject = BehaviorSubject.create(); - - restartableLatestCache(GET_LIBRARY, - this::getLibraryObservable, - (view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second)); - - if (savedState == null) { - start(GET_LIBRARY); - } - } - - @Override - protected void onDropView() { - EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class); - super.onDropView(); - } - - @Override - protected void onTakeView(LibraryFragment libraryFragment) { - super.onTakeView(libraryFragment); - if (isUnsubscribed(GET_LIBRARY)) { - start(GET_LIBRARY); - } - } - - private Observable, Map>>> getLibraryObservable() { - return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), - Pair::create) - .observeOn(AndroidSchedulers.mainThread()); - } - - private Observable> getCategoriesObservable() { - return db.getCategories().asRxObservable() - .doOnNext(categories -> this.categories = categories); - } - - private Observable>> getLibraryMangasObservable() { - return db.getLibraryMangas().asRxObservable() - .flatMap(mangas -> Observable.from(mangas) - .groupBy(manga -> manga.category) - .flatMap(group -> group.toList() - .map(list -> Pair.create(group.getKey(), list))) - .toMap(pair -> pair.first, pair -> pair.second)); - } - - public void onOpenManga(Manga manga) { - // Avoid further db updates for the library when it's not needed - stop(GET_LIBRARY); - } - - 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 deleteMangas() { - for (Manga manga : selectedMangas) { - manga.favorite = false; - } - - db.insertMangas(selectedMangas).executeAsBlocking(); - } - - 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); - } - - /** - * Update cover with local file - */ - public boolean editCoverWithLocalFile(File file, Manga manga) throws IOException { - if (!manga.initialized) - return false; - - if (manga.favorite) { - coverCache.copyToLocalCache(manga.thumbnail_url, file); - return true; - } - return false; - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt new file mode 100644 index 0000000000..23269398fd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -0,0 +1,227 @@ +package eu.kanade.tachiyomi.ui.library + +import android.os.Bundle +import android.util.Pair +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.source.SourceManager +import eu.kanade.tachiyomi.event.LibraryMangasEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import org.greenrobot.eventbus.EventBus +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.subjects.BehaviorSubject +import java.io.File +import java.io.IOException +import java.util.* +import javax.inject.Inject + +/** + * Presenter of [LibraryFragment]. + */ +class LibraryPresenter : BasePresenter() { + + /** + * Categories of the library. + */ + lateinit var categories: List + + /** + * Currently selected manga. + */ + lateinit var selectedMangas: MutableList + + /** + * Search query of the library. + */ + lateinit var searchSubject: BehaviorSubject + + /** + * Database. + */ + @Inject lateinit var db: DatabaseHelper + + /** + * Preferences. + */ + @Inject lateinit var preferences: PreferencesHelper + + /** + * Cover cache. + */ + @Inject lateinit var coverCache: CoverCache + + /** + * Source manager. + */ + @Inject lateinit var sourceManager: SourceManager + + companion object { + /** + * Id of the restartable that listens for library updates. + */ + const val GET_LIBRARY = 1 + } + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + selectedMangas = ArrayList() + + searchSubject = BehaviorSubject.create() + + restartableLatestCache(GET_LIBRARY, + { getLibraryObservable() }, + { view, pair -> view.onNextLibraryUpdate(pair.first, pair.second) }) + + if (savedState == null) { + start(GET_LIBRARY) + } + + } + + override fun onDropView() { + EventBus.getDefault().removeStickyEvent(LibraryMangasEvent::class.java) + super.onDropView() + } + + override fun onTakeView(libraryFragment: LibraryFragment) { + super.onTakeView(libraryFragment) + if (isUnsubscribed(GET_LIBRARY)) { + start(GET_LIBRARY) + } + } + + /** + * Get the categories and all its manga from the database. + * + * @return an observable of the categories and its manga. + */ + fun getLibraryObservable(): Observable, Map>>> { + return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), + { a, b -> Pair(a, b) }) + .observeOn(AndroidSchedulers.mainThread()) + } + + /** + * Get the categories from the database. + * + * @return an observable of the categories. + */ + fun getCategoriesObservable(): Observable> { + return db.categories.asRxObservable() + .doOnNext { categories -> this.categories = categories } + } + + /** + * Get the manga grouped by categories. + * + * @return an observable containing a map with the category id as key and a list of manga as the + * value. + */ + fun getLibraryMangasObservable(): Observable>> { + return db.libraryMangas.asRxObservable() + .flatMap { mangas -> Observable.from(mangas) + .groupBy { it.category } + .flatMap { group -> group.toList().map { Pair(group.key, it) } } + .toMap({ it.first }, { it.second }) + } + } + + /** + * Called when a manga is opened. + */ + fun onOpenManga() { + // Avoid further db updates for the library when it's not needed + stop(GET_LIBRARY) + } + + /** + * 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) + } else { + selectedMangas.remove(manga) + } + } + + /** + * Get the category names as a list. + */ + fun getCategoryNames(): List { + return categories.map { it.name } + } + + /** + * Remove the selected manga from the library. + */ + fun deleteMangas() { + for (manga in selectedMangas) { + manga.favorite = false + } + + db.insertMangas(selectedMangas).executeAsBlocking() + } + + /** + * Move the given list of manga to categories. + * + * @param positions the indexes of the selected categories. + * @param mangas the list of manga to move. + */ + fun moveMangasToCategories(positions: Array, mangas: List) { + val categoriesToAdd = ArrayList() + for (index in positions) { + categoriesToAdd.add(categories[index]) + } + + moveMangasToCategories(categoriesToAdd, mangas) + } + + /** + * 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, mangas: List) { + val mc = ArrayList() + + for (manga in mangas) { + for (cat in categories) { + mc.add(MangaCategory.create(manga, cat)) + } + } + + db.setMangaCategories(mc, mangas) + } + + /** + * Update cover with local file. + * + * @param file the new cover. + * @param manga the manga edited. + * @return true if the cover is updated, false otherwise + */ + @Throws(IOException::class) + fun editCoverWithLocalFile(file: File, manga: Manga): Boolean { + if (!manga.initialized) + return false + + if (manga.favorite) { + coverCache.copyToLocalCache(manga.thumbnail_url, file) + return true + } + return false + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index c320839189..fcd1bb7142 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.util import android.app.AlarmManager import android.app.Notification +import android.app.NotificationManager import android.content.Context import android.support.annotation.StringRes import android.support.v4.app.NotificationCompat @@ -27,6 +28,12 @@ inline fun Context.notification(func: NotificationCompat.Builder.() -> Unit): No return builder.build() } +/** + * Property to get the notification manager from the context. + */ +val Context.notificationManager : NotificationManager + get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + /** * Property to get the alarm manager from the context. * @return the alarm manager. diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewGroupExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewGroupExtensions.kt index 377fd1bf54..21ccf5ae3a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewGroupExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewGroupExtensions.kt @@ -12,4 +12,4 @@ import android.view.ViewGroup */ fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): View { return LayoutInflater.from(context).inflate(layout, this, attachToRoot) -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_library_category.xml b/app/src/main/res/layout/fragment_library_category.xml index f9d23bb341..06ab5a9151 100644 --- a/app/src/main/res/layout/fragment_library_category.xml +++ b/app/src/main/res/layout/fragment_library_category.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent"> diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java index 0752324049..6c159c6515 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java +++ b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.java @@ -47,7 +47,7 @@ public class LibraryUpdateServiceTest { source = mock(Source.class); when(service.sourceManager.get(anyInt())).thenReturn(source); } - + @Test public void testLifecycle() { // Smoke test