Add drawer to filter and sort the library (#570)

* Add additional drawer to filter and sort the library

* Tint icon when there's a filter active

* Comments and minor changes
This commit is contained in:
inorichi
2016-12-11 12:43:44 +01:00
committed by GitHub
parent 2dd58e5f7d
commit b067096fc7
17 changed files with 743 additions and 132 deletions

View File

@@ -221,6 +221,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
// Setup filters button
menu.findItem(R.id.action_set_filter).apply {
icon.mutate()
if (presenter.source.filters.isEmpty()) {
isEnabled = false
icon.alpha = 128

View File

@@ -3,25 +3,27 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v4.view.ViewPager
import android.support.v4.widget.DrawerLayout
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.view.*
import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference
import eu.kanade.tachiyomi.Constants
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.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.preference.invert
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.inflate
import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_library.*
@@ -81,6 +83,30 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
var mangaPerRow = 0
private set
/**
* Navigation view containing filter/sort/display items.
*/
private lateinit var navView: LibraryNavigationView
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private val drawerListener by lazy {
object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerClosed(drawerView: View) {
if (drawerView == navView) {
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
}
}
override fun onDrawerOpened(drawerView: View) {
if (drawerView == navView) {
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
}
}
}
}
/**
* Subscription for the number of manga per row.
*/
@@ -149,6 +175,25 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe { reattachAdapter() }
// Inflate and prepare drawer
navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
activity.drawer.addView(navView)
activity.drawer.addDrawerListener(drawerListener)
navView.post {
if (isAdded && !activity.drawer.isDrawerOpen(navView))
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
}
navView.onGroupClicked = { group ->
when (group) {
is LibraryNavigationView.FilterGroup -> onFilterChanged()
is LibraryNavigationView.SortGroup -> onSortChanged()
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
}
}
}
override fun onResume() {
@@ -157,6 +202,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
}
override fun onDestroyView() {
activity.drawer.removeDrawerListener(drawerListener)
activity.drawer.removeView(navView)
numColumnsSubscription?.unsubscribe()
tabs.setupWithViewPager(null)
tabs.visibility = View.GONE
@@ -169,34 +216,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
super.onSaveInstanceState(outState)
}
/**
* Prepare the Fragment host's standard options menu to be displayed. This is
* called right before the menu is shown, every time it is shown. You can
* use this method to efficiently enable/disable items or otherwise
* dynamically modify the contents.
*
* @param menu The options menu as last shown or first initialized by
*/
override fun onPrepareOptionsMenu(menu: Menu) {
// Initialize search menu
val filterDownloadedItem = menu.findItem(R.id.action_filter_downloaded)
val filterUnreadItem = menu.findItem(R.id.action_filter_unread)
val sortModeAlpha = menu.findItem(R.id.action_sort_alpha)
val sortModeLastRead = menu.findItem(R.id.action_sort_last_read)
val sortModeLastUpdated = menu.findItem(R.id.action_sort_last_updated)
// Set correct checkbox filter
filterDownloadedItem.isChecked = preferences.filterDownloaded().getOrDefault()
filterUnreadItem.isChecked = preferences.filterUnread().getOrDefault()
// Set correct radio button sort
when (preferences.librarySortingMode().getOrDefault()) {
Constants.SORT_LIBRARY_ALPHA -> sortModeAlpha.isChecked = true
Constants.SORT_LIBRARY_LAST_READ -> sortModeLastRead.isChecked = true
Constants.SORT_LIBRARY_LAST_UPDATED -> sortModeLastUpdated.isChecked = true
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu)
@@ -209,6 +228,9 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
searchView.clearFocus()
}
// Mutate the filter icon because it needs to be tinted and the resource is shared.
menu.findItem(R.id.action_filter).icon.mutate()
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
onSearchTextChange(query)
@@ -223,40 +245,19 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
}
override fun onPrepareOptionsMenu(menu: Menu) {
val filterItem = menu.findItem(R.id.action_filter)
// Tint icon if there's a filter active
val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
DrawableCompat.setTint(filterItem.icon, filterColor)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_filter_unread -> {
// Update settings.
preferences.filterUnread().invert()
// Apply filter.
onFilterOrSortChanged()
R.id.action_filter -> {
activity.drawer.openDrawer(Gravity.END)
}
R.id.action_filter_downloaded -> {
// Update settings.
preferences.filterDownloaded().invert()
// Apply filter.
onFilterOrSortChanged()
}
R.id.action_filter_empty -> {
// Update settings.
preferences.filterUnread().set(false)
preferences.filterDownloaded().set(false)
// Apply filter
onFilterOrSortChanged()
}
R.id.action_sort_alpha -> {
preferences.librarySortingMode().set(Constants.SORT_LIBRARY_ALPHA)
onFilterOrSortChanged()
}
R.id.action_sort_last_read -> {
preferences.librarySortingMode().set(Constants.SORT_LIBRARY_LAST_READ)
onFilterOrSortChanged()
}
R.id.action_sort_last_updated -> {
preferences.librarySortingMode().set(Constants.SORT_LIBRARY_LAST_UPDATED)
onFilterOrSortChanged()
}
R.id.action_library_display_mode -> swapDisplayMode()
R.id.action_update_library -> {
LibraryUpdateService.start(activity)
}
@@ -271,19 +272,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
}
/**
* Applies filter change
* Called when a filter is changed.
*/
private fun onFilterOrSortChanged() {
private fun onFilterChanged() {
presenter.requestLibraryUpdate()
activity.supportInvalidateOptionsMenu()
}
/**
* Swap display mode
* Called when the sorting mode is changed.
*/
private fun swapDisplayMode() {
presenter.swapDisplayMode()
reattachAdapter()
private fun onSortChanged() {
presenter.requestLibraryUpdate()
}
/**

View File

@@ -0,0 +1,187 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.util.AttributeSet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
import uy.kohesive.injekt.injectLazy
/**
* The navigation view shown in a drawer with the different options to show the library.
*/
class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: ExtendedNavigationView(context, attrs) {
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* List of groups shown in the view.
*/
private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup())
/**
* Adapter instance.
*/
private val adapter = Adapter(groups.map { it.createItems() }.flatten())
/**
* Click listener to notify the parent fragment when an item from a group is clicked.
*/
var onGroupClicked: (Group) -> Unit = {}
init {
recycler.adapter = adapter
groups.forEach { it.initModels() }
}
/**
* Returns true if there's at least one filter from [FilterGroup] active.
*/
fun hasActiveFilters(): Boolean {
return (groups[0] as FilterGroup).items.any { it.checked }
}
/**
* Adapter of the recycler view.
*/
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
override fun onItemClicked(item: Item) {
if (item is GroupedItem) {
item.group.onItemClicked(item)
onGroupClicked(item.group)
}
}
}
/**
* Filters group (unread, downloaded, ...).
*/
inner class FilterGroup : Group {
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
override val items = listOf(downloaded, unread)
override val header = Item.Header(R.string.action_filter)
override val footer = Item.Separator()
override fun initModels() {
downloaded.checked = preferences.filterDownloaded().getOrDefault()
unread.checked = preferences.filterUnread().getOrDefault()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
downloaded -> preferences.filterDownloaded().set(item.checked)
unread -> preferences.filterUnread().set(item.checked)
}
adapter.notifyItemChanged(item)
}
}
/**
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
*/
inner class SortGroup : Group {
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
override val items = listOf(alphabetically, lastRead, lastUpdated)
override val header = Item.Header(R.string.action_sort)
override val footer = Item.Separator()
override fun initModels() {
val sorting = preferences.librarySortingMode().getOrDefault()
val order = if (preferences.librarySortingAscending().getOrDefault())
SORT_ASC else SORT_DESC
alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
}
override fun onItemClicked(item: Item) {
item as Item.MultiStateGroup
val prevState = item.state
item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
item.state = when (prevState) {
SORT_NONE -> SORT_ASC
SORT_ASC -> SORT_DESC
SORT_DESC -> SORT_ASC
else -> throw Exception("Unknown state")
}
preferences.librarySortingMode().set(when (item) {
alphabetically -> LibrarySort.ALPHA
lastRead -> LibrarySort.LAST_READ
lastUpdated -> LibrarySort.LAST_UPDATED
else -> throw Exception("Unknown sorting")
})
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
/**
* Display group, to show the library as a list or a grid.
*/
inner class DisplayGroup : Group {
private val grid = Item.Radio(R.string.action_display_grid, this)
private val list = Item.Radio(R.string.action_display_list, this)
override val items = listOf(grid, list)
override val header = Item.Header(R.string.action_display)
override val footer = null
override fun initModels() {
val asList = preferences.libraryAsList().getOrDefault()
grid.checked = !asList
list.checked = asList
}
override fun onItemClicked(item: Item) {
item as Item.Radio
if (item.checked) return
item.group.items.forEach { (it as Item.Radio).checked = false }
item.checked = true
preferences.libraryAsList().set(if (item == list) true else false)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
}

View File

@@ -4,7 +4,6 @@ import android.os.Bundle
import android.util.Pair
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@@ -111,9 +110,12 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
}
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
val isAscending = preferences.librarySortingAscending().getOrDefault()
val comparator = Comparator<Manga> { m1, m2 -> sortManga(m1, m2) }
return map.mapValues { entry -> entry.value
.filter { filterManga(it) }
.sortedWith(Comparator<Manga> { m1, m2 -> sortManga(m1, m2) })
.sortedWith(if (isAscending) comparator else Collections.reverseOrder(comparator))
}
}
@@ -172,19 +174,15 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
*/
fun sortManga(manga1: Manga, manga2: Manga): Int {
when (preferences.librarySortingMode().getOrDefault()) {
Constants.SORT_LIBRARY_ALPHA -> return manga1.title.compareTo(manga2.title)
Constants.SORT_LIBRARY_LAST_READ -> {
LibrarySort.ALPHA -> return manga1.title.compareTo(manga2.title)
LibrarySort.LAST_READ -> {
var a = 0L
var b = 0L
manga1.id?.let { manga1Id ->
manga2.id?.let { manga2Id ->
db.getLastHistoryByMangaId(manga1Id).executeAsBlocking()?.let { a = it.last_read }
db.getLastHistoryByMangaId(manga2Id).executeAsBlocking()?.let { b = it.last_read }
}
}
db.getLastHistoryByMangaId(manga1.id!!).executeAsBlocking()?.let { a = it.last_read }
db.getLastHistoryByMangaId(manga2.id!!).executeAsBlocking()?.let { b = it.last_read }
return b.compareTo(a)
}
Constants.SORT_LIBRARY_LAST_UPDATED -> return manga2.last_update.compareTo(manga1.last_update)
LibrarySort.LAST_UPDATED -> return manga2.last_update.compareTo(manga1.last_update)
else -> return manga1.title.compareTo(manga2.title)
}
}
@@ -326,12 +324,4 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
return false
}
/**
* Changes the active display mode.
*/
fun swapDisplayMode() {
val displayAsList = preferences.libraryAsList().getOrDefault()
preferences.libraryAsList().set(!displayAsList)
}
}

View File

@@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.ui.library
object LibrarySort {
const val ALPHA = 0
const val LAST_READ = 1
const val LAST_UPDATED = 2
}