mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Migrate library to Kotlin.
This commit is contained in:
		| @@ -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. | ||||
|      */ | ||||
|   | ||||
| @@ -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<Integer, List<Manga>> mangas; | ||||
|  | ||||
|     public LibraryMangasEvent(Map<Integer, List<Manga>> mangas) { | ||||
|         this.mangas = mangas; | ||||
|     } | ||||
|  | ||||
|     public Map<Integer, List<Manga>> getMangas() { | ||||
|         return mangas; | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     public List<Manga> getMangasForCategory(Category category) { | ||||
|         return mangas.get(category.id); | ||||
|     } | ||||
| } | ||||
| @@ -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<Int, List<Manga>>) { | ||||
|  | ||||
|     fun getMangasForCategory(category: Category): List<Manga>? { | ||||
|         return mangas[category.id] | ||||
|     } | ||||
| } | ||||
| @@ -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<CategoryPresenter>(), ActionMode.Callbac | ||||
|      * Call this when action mode action is finished. | ||||
|      */ | ||||
|     fun destroyActionModeIfNeeded() { | ||||
|             actionMode?.finish() | ||||
|         actionMode?.finish() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -163,8 +163,8 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), 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<CategoryPresenter>(), 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<CategoryPresenter>(), 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<CategoryPresenter>(), 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<CategoryPresenter>(), 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 { | ||||
|   | ||||
| @@ -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<Category> 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<Category> categories) { | ||||
|         if (this.categories != categories) { | ||||
|             this.categories = categories; | ||||
|             notifyDataSetChanged(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setSelectionMode(int mode) { | ||||
|         for (Fragment fragment : getRegisteredFragments()) { | ||||
|             ((LibraryCategoryFragment) fragment).setMode(mode); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<Category>? = 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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<LibraryHolder, Manga> { | ||||
|  | ||||
|     private List<Manga> mangas; | ||||
|     private LibraryCategoryFragment fragment; | ||||
|  | ||||
|     public LibraryCategoryAdapter(LibraryCategoryFragment fragment) { | ||||
|         this.fragment = fragment; | ||||
|         mItems = new ArrayList<>(); | ||||
|         setHasStableIds(true); | ||||
|     } | ||||
|  | ||||
|     public void setItems(List<Manga> 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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<LibraryHolder, Manga>() { | ||||
|  | ||||
|     /** | ||||
|      * The list of manga in this category. | ||||
|      */ | ||||
|     private var mangas: List<Manga>? = null | ||||
|  | ||||
|     init { | ||||
|         setHasStableIds(true) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets a list of manga in the adapter. | ||||
|      * | ||||
|      * @param list the list to set. | ||||
|      */ | ||||
|     fun setItems(list: List<Manga>) { | ||||
|         mItems = list | ||||
|  | ||||
|         // A copy of manga 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 | ||||
|  | ||||
| } | ||||
| @@ -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<Manga> 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<Integer> 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<Category> 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<Manga> 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<Manga> 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(); | ||||
|     } | ||||
| } | ||||
| @@ -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<Manga>? = 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<Int> { | ||||
|         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 | ||||
|  | ||||
| } | ||||
| @@ -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<LibraryPresenter> | ||||
|         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<Category> categories, Map<Integer, List<Manga>> 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<Category> categories) { | ||||
|         List<Category> categoriesWithDefault = new ArrayList<>(); | ||||
|         categoriesWithDefault.add(Category.createDefault()); | ||||
|         categoriesWithDefault.addAll(categories); | ||||
|  | ||||
|         setCategories(categoriesWithDefault); | ||||
|     } | ||||
|  | ||||
|     private void setCategories(List<Category> 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<Manga> 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<Manga> 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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<LibraryPresenter>(), 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<Category>, mangaMap: Map<Int, List<Manga>>) { | ||||
|         // 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<Category>) { | ||||
|         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<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 { | ||||
|                 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<Manga>) { | ||||
|         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() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<LibraryFragment> { | ||||
|  | ||||
|     private static final int GET_LIBRARY = 1; | ||||
|     protected List<Category> categories; | ||||
|     protected List<Manga> selectedMangas; | ||||
|     protected BehaviorSubject<String> 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<Pair<List<Category>, Map<Integer, List<Manga>>>> getLibraryObservable() { | ||||
|         return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(), | ||||
|                 Pair::create) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()); | ||||
|     } | ||||
|  | ||||
|     private Observable<List<Category>> getCategoriesObservable() { | ||||
|         return db.getCategories().asRxObservable() | ||||
|                 .doOnNext(categories -> this.categories = categories); | ||||
|     } | ||||
|  | ||||
|     private Observable<Map<Integer, List<Manga>>> 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<Manga> mangas) { | ||||
|         List<Category> categoriesToAdd = new ArrayList<>(); | ||||
|         for (Integer index : positions) { | ||||
|             categoriesToAdd.add(categories.get(index)); | ||||
|         } | ||||
|  | ||||
|         moveMangasToCategories(categoriesToAdd, mangas); | ||||
|     } | ||||
|  | ||||
|     public void moveMangasToCategories(List<Category> categories, List<Manga> mangas) { | ||||
|         List<MangaCategory> 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; | ||||
|     } | ||||
| } | ||||
| @@ -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<LibraryFragment>() { | ||||
|  | ||||
|     /** | ||||
|      * Categories of the library. | ||||
|      */ | ||||
|     lateinit var categories: List<Category> | ||||
|  | ||||
|     /** | ||||
|      * Currently selected manga. | ||||
|      */ | ||||
|     lateinit var selectedMangas: MutableList<Manga> | ||||
|  | ||||
|     /** | ||||
|      * Search query of the library. | ||||
|      */ | ||||
|     lateinit var searchSubject: BehaviorSubject<String> | ||||
|  | ||||
|     /** | ||||
|      * 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<Pair<List<Category>, Map<Int, List<Manga>>>> { | ||||
|         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<List<Category>> { | ||||
|         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<Map<Int, List<Manga>>> { | ||||
|         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<String> { | ||||
|         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<Int>, mangas: List<Manga>) { | ||||
|         val categoriesToAdd = ArrayList<Category>() | ||||
|         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<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 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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user