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

@ -7,7 +7,4 @@ object Constants {
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4 const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5 const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
const val SORT_LIBRARY_ALPHA = 0
const val SORT_LIBRARY_LAST_READ = 1
const val SORT_LIBRARY_LAST_UPDATED = 2
} }

View file

@ -130,6 +130,8 @@ class PreferencesHelper(context: Context) {
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0) fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false) fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())

View file

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

View file

@ -3,25 +3,27 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.TabLayout import android.support.design.widget.TabLayout
import android.support.v4.graphics.drawable.DrawableCompat
import android.support.v4.view.ViewPager import android.support.v4.view.ViewPager
import android.support.v4.widget.DrawerLayout
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import eu.kanade.tachiyomi.Constants
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.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.category.CategoryActivity
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_library.* import kotlinx.android.synthetic.main.fragment_library.*
@ -81,6 +83,30 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
var mangaPerRow = 0 var mangaPerRow = 0
private set 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. * Subscription for the number of manga per row.
*/ */
@ -149,6 +175,25 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
.skip(1) .skip(1)
// Set again the adapter to recalculate the covers height // Set again the adapter to recalculate the covers height
.subscribe { reattachAdapter() } .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() { override fun onResume() {
@ -157,6 +202,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
} }
override fun onDestroyView() { override fun onDestroyView() {
activity.drawer.removeDrawerListener(drawerListener)
activity.drawer.removeView(navView)
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
tabs.setupWithViewPager(null) tabs.setupWithViewPager(null)
tabs.visibility = View.GONE tabs.visibility = View.GONE
@ -169,34 +216,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
super.onSaveInstanceState(outState) 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) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu) inflater.inflate(R.menu.library, menu)
@ -209,6 +228,9 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
searchView.clearFocus() 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 { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
onSearchTextChange(query) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_filter_unread -> { R.id.action_filter -> {
// Update settings. activity.drawer.openDrawer(Gravity.END)
preferences.filterUnread().invert()
// Apply filter.
onFilterOrSortChanged()
} }
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 -> { R.id.action_update_library -> {
LibraryUpdateService.start(activity) 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() presenter.requestLibraryUpdate()
activity.supportInvalidateOptionsMenu() activity.supportInvalidateOptionsMenu()
} }
/** /**
* Swap display mode * Called when the sorting mode is changed.
*/ */
private fun swapDisplayMode() { private fun onSortChanged() {
presenter.swapDisplayMode() presenter.requestLibraryUpdate()
reattachAdapter()
} }
/** /**

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 android.util.Pair
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category 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>> { 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 return map.mapValues { entry -> entry.value
.filter { filterManga(it) } .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 { fun sortManga(manga1: Manga, manga2: Manga): Int {
when (preferences.librarySortingMode().getOrDefault()) { when (preferences.librarySortingMode().getOrDefault()) {
Constants.SORT_LIBRARY_ALPHA -> return manga1.title.compareTo(manga2.title) LibrarySort.ALPHA -> return manga1.title.compareTo(manga2.title)
Constants.SORT_LIBRARY_LAST_READ -> { LibrarySort.LAST_READ -> {
var a = 0L var a = 0L
var b = 0L var b = 0L
manga1.id?.let { manga1Id -> db.getLastHistoryByMangaId(manga1.id!!).executeAsBlocking()?.let { a = it.last_read }
manga2.id?.let { manga2Id -> db.getLastHistoryByMangaId(manga2.id!!).executeAsBlocking()?.let { b = it.last_read }
db.getLastHistoryByMangaId(manga1Id).executeAsBlocking()?.let { a = it.last_read }
db.getLastHistoryByMangaId(manga2Id).executeAsBlocking()?.let { b = it.last_read }
}
}
return b.compareTo(a) 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) else -> return manga1.title.compareTo(manga2.title)
} }
} }
@ -326,12 +324,4 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
return false 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
}

View file

@ -0,0 +1,363 @@
package eu.kanade.tachiyomi.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.support.annotation.CallSuper
import android.support.design.R
import android.support.design.internal.ScrimInsetsFrameLayout
import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.TintTypedArray
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.CheckedTextView
import android.widget.RadioButton
import android.widget.TextView
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.R as TR
/**
* An alternative implementation of [android.support.design.widget.NavigationView], without menu
* inflation and allowing customizable items (multiple selections, custom views, etc).
*/
@Suppress("LeakingThis")
@SuppressLint("PrivateResource")
open class ExtendedNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: ScrimInsetsFrameLayout(context, attrs, defStyleAttr) {
/**
* Max width of the navigation view.
*/
private var maxWidth: Int
/**
* Recycler view containing all the items.
*/
protected val recycler = RecyclerView(context)
init {
// Custom attributes
val a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.NavigationView, defStyleAttr,
R.style.Widget_Design_NavigationView)
ViewCompat.setBackground(
this, a.getDrawable(R.styleable.NavigationView_android_background))
if (a.hasValue(R.styleable.NavigationView_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.NavigationView_elevation, 0).toFloat())
}
ViewCompat.setFitsSystemWindows(this,
a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false))
maxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0)
a.recycle()
recycler.layoutManager = LinearLayoutManager(context)
addView(recycler)
}
/**
* Overriden to measure the width of the navigation view.
*/
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val width = when (MeasureSpec.getMode(widthSpec)) {
MeasureSpec.AT_MOST -> MeasureSpec.makeMeasureSpec(
Math.min(MeasureSpec.getSize(widthSpec), maxWidth), MeasureSpec.EXACTLY)
MeasureSpec.UNSPECIFIED -> MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY)
else -> widthSpec
}
// Let super sort out the height
super.onMeasure(width, heightSpec)
}
/**
* Every item of the nav view. Generic items must belong to this list, custom items could be
* implemented by an abstract class. If more customization is needed in the future, this can be
* changed to an interface instead of sealed class.
*/
sealed class Item {
/**
* A view separator.
*/
class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
/**
* A header with a title.
*/
class Header(val resTitle: Int) : Item()
/**
* A checkbox.
*/
open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item()
/**
* A checkbox belonging to a group. The group must handle selections and restrictions.
*/
class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false)
: Checkbox(resTitle, checked), GroupedItem
/**
* A radio belonging to a group (a sole radio makes no sense). The group must handle
* selections and restrictions.
*/
class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false)
: Item(), GroupedItem
/**
* An item with which needs more than two states (selected/deselected).
*/
abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() {
/**
* Returns the drawable associated to every possible each state.
*/
abstract fun getStateDrawable(context: Context): Drawable?
/**
* Creates a vector tinted with the accent color.
*
* @param context any context.
* @param resId the vector resource to load and tint
*/
fun tintVector(context: Context, resId: Int): Drawable {
return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply {
setTint(context.theme.getResourceColor(TR.attr.colorAccent))
}
}
}
/**
* An item with which needs more than two states (selected/deselected) belonging to a group.
* The group must handle selections and restrictions.
*/
abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0)
: MultiState(resTitle, state), GroupedItem
/**
* A multistate item for sorting lists (unselected, ascending, descending).
*/
class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
companion object {
const val SORT_NONE = 0
const val SORT_ASC = 1
const val SORT_DESC = 2
}
override fun getStateDrawable(context: Context): Drawable? {
return when (state) {
SORT_ASC -> tintVector(context, TR.drawable.ic_keyboard_arrow_up_black_32dp)
SORT_DESC -> tintVector(context, TR.drawable.ic_keyboard_arrow_down_black_32dp)
SORT_NONE -> ContextCompat.getDrawable(context, TR.drawable.empty_drawable_32dp)
else -> null
}
}
}
}
/**
* Interface for an item belonging to a group.
*/
interface GroupedItem {
val group: Group
}
/**
* A group containing a list of items.
*/
interface Group {
/**
* An optional header for the group, typically a [Item.Header].
*/
val header: Item?
/**
* An optional footer for the group, typically a [Item.Separator].
*/
val footer: Item?
/**
* The items of the group, excluding header and footer.
*/
val items: List<Item>
/**
* Creates all the elements of this group. Implementations can override this method for more
* customization.
*/
fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
/**
* Called after creating the list of items. Implementations should load the current values
* into the models.
*/
fun initModels()
/**
* Called when an item of this group is clicked. The group is responsible for all the
* selections of its items.
*/
fun onItemClicked(item: Item)
}
/**
* Base view holder.
*/
abstract class Holder(view: View) : RecyclerView.ViewHolder(view)
/**
* Separator view holder.
*/
class SeparatorHolder(parent: ViewGroup)
: Holder(parent.inflate(R.layout.design_navigation_item_separator))
/**
* Header view holder.
*/
class HeaderHolder(parent: ViewGroup)
: Holder(parent.inflate(R.layout.design_navigation_item_subheader))
/**
* Clickable view holder.
*/
abstract class ClickableHolder(view: View, listener: View.OnClickListener?) : Holder(view) {
init {
itemView.setOnClickListener(listener)
}
}
/**
* Radio view holder.
*/
class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?)
: ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) {
val radio = itemView.findViewById(TR.id.nav_view_item) as RadioButton
}
/**
* Checkbox view holder.
*/
class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?)
: ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) {
val check = itemView.findViewById(TR.id.nav_view_item) as CheckBox
}
/**
* Multi state view holder.
*/
class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?)
: ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) {
val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView
}
/**
* Base adapter for the navigation view. It knows how to create and render every subclass of
* [Item].
*/
abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
private val onClick = View.OnClickListener {
val pos = recycler.getChildAdapterPosition(it)
val item = items[pos]
onItemClicked(item)
}
fun notifyItemChanged(item: Item) {
val pos = items.indexOf(item)
if (pos != -1) notifyItemChanged(pos)
}
override fun getItemCount(): Int {
return items.size
}
@CallSuper
override fun getItemViewType(position: Int): Int {
val item = items[position]
return when (item) {
is Item.Header -> VIEW_TYPE_HEADER
is Item.Separator -> VIEW_TYPE_SEPARATOR
is Item.Radio -> VIEW_TYPE_RADIO
is Item.Checkbox -> VIEW_TYPE_CHECKBOX
is Item.MultiState -> VIEW_TYPE_MULTISTATE
}
}
@CallSuper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return when (viewType) {
VIEW_TYPE_HEADER -> HeaderHolder(parent)
VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
else -> throw Exception("Unknown view type")
}
}
@CallSuper
override fun onBindViewHolder(holder: Holder, position: Int) {
when (holder) {
is HeaderHolder -> {
val view = holder.itemView as TextView
val item = items[position] as Item.Header
view.setText(item.resTitle)
}
is SeparatorHolder -> {
val view = holder.itemView
val item = items[position] as Item.Separator
view.setPadding(0, item.paddingTop, 0, item.paddingBottom)
}
is RadioHolder -> {
val item = items[position] as Item.Radio
holder.radio.setText(item.resTitle)
holder.radio.isChecked = item.checked
}
is CheckboxHolder -> {
val item = items[position] as Item.CheckboxGroup
holder.check.setText(item.resTitle)
holder.check.isChecked = item.checked
}
is MultiStateHolder -> {
val item = items[position] as Item.MultiStateGroup
val drawable = item.getStateDrawable(context)
holder.text.setText(item.resTitle)
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
}
}
}
abstract fun onItemClicked(item: Item)
}
companion object {
private const val VIEW_TYPE_HEADER = 100
private const val VIEW_TYPE_SEPARATOR = 101
private const val VIEW_TYPE_RADIO = 102
private const val VIEW_TYPE_CHECKBOX = 103
private const val VIEW_TYPE_MULTISTATE = 104
}
}

View file

@ -0,0 +1,8 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
<size
android:width="32dp"
android:height="32dp" />
</shape>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.library.LibraryNavigationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_view2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:fitsSystemWindows="false" />

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:foreground="?attr/selectableItemBackground"
android:focusable="true">
<CheckBox
android:id="@+id/nav_view_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:background="@android:color/transparent"
android:gravity="center_vertical|start"
android:maxLines="1"
android:clickable="false"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
</LinearLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:foreground="?attr/selectableItemBackground"
android:focusable="true">
<CheckedTextView
android:id="@+id/nav_view_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawablePadding="@dimen/material_component_lists_icon_left_padding"
android:gravity="center_vertical|start"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
</LinearLayout>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:foreground="?attr/selectableItemBackground"
android:focusable="true">
<RadioButton
android:id="@+id/nav_view_item"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:background="@android:color/transparent"
android:gravity="center_vertical|start"
android:maxLines="1"
android:clickable="false"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
</LinearLayout>

View file

@ -14,44 +14,7 @@
android:id="@+id/action_filter" android:id="@+id/action_filter"
android:icon="@drawable/ic_filter_list_white_24dp" android:icon="@drawable/ic_filter_list_white_24dp"
android:title="@string/action_filter" android:title="@string/action_filter"
app:showAsAction="ifRoom"> app:showAsAction="ifRoom"/>
<menu>
<item
android:id="@+id/action_filter_downloaded"
android:checkable="true"
android:title="@string/action_filter_downloaded"/>
<item
android:id="@+id/action_filter_unread"
android:checkable="true"
android:title="@string/action_filter_unread"/>
<item
android:id="@+id/action_filter_empty"
android:title="@string/action_filter_empty"/>
</menu>
</item>
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_sort_white_24dp"
android:title="@string/action_sort"
app:showAsAction="never"
>
<menu>
<group
android:id="@+id/sort_group"
android:checkableBehavior="single">
<item
android:id="@+id/action_sort_alpha"
android:title="@string/action_sort_alpha"/>
<item
android:id="@+id/action_sort_last_read"
android:title="@string/action_sort_last_read"/>
<item
android:id="@+id/action_sort_last_updated"
android:title="@string/action_sort_last_updated"/>
</group>
</menu>
</item>
<item <item
android:id="@+id/action_update_library" android:id="@+id/action_update_library"
@ -59,11 +22,6 @@
android:title="@string/action_update_library" android:title="@string/action_update_library"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_library_display_mode"
android:title="@string/action_display_mode"
app:showAsAction="never"/>
<item <item
android:id="@+id/action_edit_categories" android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories" android:title="@string/action_edit_categories"

View file

@ -60,6 +60,9 @@
<string name="action_open_in_browser">Open in browser</string> <string name="action_open_in_browser">Open in browser</string>
<string name="action_add_to_home_screen">Add to home screen</string> <string name="action_add_to_home_screen">Add to home screen</string>
<string name="action_display_mode">Change display mode</string> <string name="action_display_mode">Change display mode</string>
<string name="action_display">Display</string>
<string name="action_display_grid">Grid</string>
<string name="action_display_list">List</string>
<string name="action_set_filter">Set filter</string> <string name="action_set_filter">Set filter</string>
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_sort">Sort</string> <string name="action_sort">Sort</string>