mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 21:11:52 +01:00
Merge pull request #180 from inorichi/library-kotlin
Migrate UI library to Kotlin.
This commit is contained in:
commit
19eb77f049
@ -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
|
||||
@ -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,16 +192,15 @@ 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))
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate menu when action mode selected.
|
||||
@ -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.
|
||||
|
@ -5,7 +5,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
android:id="@+id/library_mangas"
|
||||
android:id="@+id/recycler"
|
||||
style="@style/AppTheme.GridView"
|
||||
android:columnWidth="140dp"
|
||||
tools:listitem="@layout/item_catalogue_grid" />
|
||||
|
Loading…
Reference in New Issue
Block a user