Merge pull request #180 from inorichi/library-kotlin

Migrate UI library to Kotlin.
This commit is contained in:
inorichi 2016-02-25 15:42:31 +01:00
commit 19eb77f049
20 changed files with 1149 additions and 925 deletions

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver 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.AndroidComponentUtil
import eu.kanade.tachiyomi.util.NetworkUtil import eu.kanade.tachiyomi.util.NetworkUtil
import eu.kanade.tachiyomi.util.notification import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -310,12 +310,6 @@ class LibraryUpdateService : Service() {
notificationManager.cancel(UPDATE_NOTIFICATION_ID) 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. * Property that returns an intent to open the main activity.
*/ */

View File

@ -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);
}
}

View File

@ -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]
}
}

View File

@ -10,12 +10,12 @@ import android.support.v7.widget.helper.ItemTouchHelper
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener 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.activity_edit_categories.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -163,8 +163,8 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
it.invalidate() it.invalidate()
// Show edit button only when one item is selected // Show edit button only when one item is selected
val editItem = it.menu?.findItem(R.id.action_edit) val editItem = it.menu.findItem(R.id.action_edit)
editItem?.isVisible = count == 1 editItem.isVisible = count == 1
} }
} }
} }
@ -192,16 +192,15 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
R.id.action_delete -> { R.id.action_delete -> {
// Delete select categories. // Delete select categories.
deleteCategories(getSelectedCategories()) deleteCategories(getSelectedCategories())
return true
} }
R.id.action_edit -> { R.id.action_edit -> {
// Edit selected category // Edit selected category
editCategory(getSelectedCategories()?.get(0)) editCategory(getSelectedCategories()?.get(0))
}
else -> return false
}
return true return true
} }
}
return false
}
/** /**
* Inflate menu when action mode selected. * Inflate menu when action mode selected.
@ -215,7 +214,7 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
// Inflate menu. // Inflate menu.
mode.menuInflater.inflate(R.menu.category_selection, menu) mode.menuInflater.inflate(R.menu.category_selection, menu)
// Enable adapter multi selection. // Enable adapter multi selection.
adapter.mode = LibraryCategoryAdapter.MODE_MULTI adapter.mode = FlexibleAdapter.MODE_MULTI
return true return true
} }
@ -226,7 +225,7 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
*/ */
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
// Reset adapter to single selection // Reset adapter to single selection
adapter.mode = LibraryCategoryAdapter.MODE_SINGLE adapter.mode = FlexibleAdapter.MODE_SINGLE
// Clear selected items // Clear selected items
adapter.clearSelection() adapter.clearSelection()
actionMode = null actionMode = null
@ -239,8 +238,9 @@ class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callbac
*/ */
override fun onListItemClick(position: Int): Boolean { override fun onListItemClick(position: Int): Boolean {
// Check if action mode is initialized and selected item exist. // Check if action mode is initialized and selected item exist.
if (actionMode != null && position != -1) { if (position == -1) {
// Toggle selection of clicked item. return false
} else if (actionMode != null) {
toggleSelection(position) toggleSelection(position)
return true return true
} else { } else {

View File

@ -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);
}
}
}

View File

@ -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()
}
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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();
}
}

View File

@ -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
}

View File

@ -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();
}
}

View File

@ -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()
}
}

View File

@ -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);
}
}
}

View File

@ -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)
}
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.util
import android.app.AlarmManager import android.app.AlarmManager
import android.app.Notification import android.app.Notification
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.support.annotation.StringRes import android.support.annotation.StringRes
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
@ -27,6 +28,12 @@ inline fun Context.notification(func: NotificationCompat.Builder.() -> Unit): No
return builder.build() 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. * Property to get the alarm manager from the context.
* @return the alarm manager. * @return the alarm manager.

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<eu.kanade.tachiyomi.widget.AutofitRecyclerView <eu.kanade.tachiyomi.widget.AutofitRecyclerView
android:id="@+id/library_mangas" android:id="@+id/recycler"
style="@style/AppTheme.GridView" style="@style/AppTheme.GridView"
android:columnWidth="140dp" android:columnWidth="140dp"
tools:listitem="@layout/item_catalogue_grid" /> tools:listitem="@layout/item_catalogue_grid" />