Эх сурвалжийг харах

Filters with flexible adapter

len 8 жил өмнө
parent
commit
90a99dde1f
26 өөрчлөгдсөн 747 нэмэгдсэн , 204 устгасан
  1. 17 2
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt
  2. 1 8
      app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt
  3. 5 5
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt
  4. 17 7
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt
  5. 3 7
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt
  6. 6 10
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt
  7. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt
  8. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
  9. 38 161
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt
  10. 49 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt
  11. 56 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt
  12. 44 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt
  13. 48 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt
  14. 58 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt
  15. 41 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt
  16. 57 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt
  17. 73 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortItem.kt
  18. 53 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt
  19. 75 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt
  20. 0 1
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt
  21. 9 0
      app/src/main/res/drawable/ic_chevron_right_white_24dp.xml
  22. 9 0
      app/src/main/res/drawable/ic_expand_more_white_24dp.xml
  23. 4 0
      app/src/main/res/layout/catalogue_drawer_content.xml
  24. 30 0
      app/src/main/res/layout/navigation_view_group.xml
  25. 30 0
      app/src/main/res/layout/navigation_view_sort.xml
  26. 21 0
      app/src/main/res/layout/navigation_view_sort_item.xml

+ 17 - 2
app/src/main/java/eu/kanade/tachiyomi/data/source/model/Filter.kt

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.source.model
 sealed class Filter<T>(val name: String, var state: T) {
     open class Header(name: String) : Filter<Any>(name, 0)
     open class Separator(name: String = "") : Filter<Any>(name, 0)
-    abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
+    abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
     abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
     abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
     abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
@@ -17,9 +17,24 @@ sealed class Filter<T>(val name: String, var state: T) {
             const val STATE_EXCLUDE = 2
         }
     }
+    abstract class Group<V>(name: String, state: List<V>): Filter<List<V>>(name, state)
 
-    abstract class Sort<V>(name: String, val values: Array<V>, state: Selection? = null)
+    abstract class Sort(name: String, val values: Array<String>, state: Selection? = null)
         : Filter<Sort.Selection?>(name, state) {
         data class Selection(val index: Int, val ascending: Boolean)
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Filter<*>) return false
+
+        return name == other.name && state == other.state
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + (state?.hashCode() ?: 0)
+        return result
+    }
+
 }

+ 1 - 8
app/src/main/java/eu/kanade/tachiyomi/data/source/model/FilterList.kt

@@ -1,14 +1,7 @@
 package eu.kanade.tachiyomi.data.source.model
 
-class FilterList(list: List<Filter<*>>) : List<Filter<*>> by list {
+data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
 
     constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
 
-    fun hasSameState(other: FilterList): Boolean {
-        if (size != other.size) return false
-
-        return (0..lastIndex)
-                .all { get(it).javaClass == other[it].javaClass && get(it).state == other[it].state }
-    }
-
 }

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt

@@ -99,7 +99,7 @@ class Batoto : ParsedOnlineSource(), LoginSource {
                 is TextField -> {
                     if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                 }
-                is ListField -> {
+                is SelectField -> {
                     val sel = filter.values[filter.state].value
                     if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
                 }
@@ -290,9 +290,9 @@ class Batoto : ParsedOnlineSource(), LoginSource {
     private class Status : Filter.TriState("Completed")
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
+    private class SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
     private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
-    private class OrderBy() : Filter.Sort<String>("Order by",
+    private class OrderBy() : Filter.Sort("Order by",
             arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
             Filter.Sort.Selection(4, false))
 
@@ -302,14 +302,14 @@ class Batoto : ParsedOnlineSource(), LoginSource {
     // on https://bato.to/search
     override fun getFilterList() = FilterList(
             TextField("Author", "artist_name"),
-            ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
+            SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
             Status(),
             Flag("Exclude mature", "mature", "m", ""),
             Filter.Separator(),
             OrderBy(),
             Filter.Separator(),
             Filter.Header("Genres"),
-            ListField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
+            SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
             Genre("4-Koma", 40),
             Genre("Action", 1),
             Genre("Adventure", 2),

+ 17 - 7
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt

@@ -58,7 +58,12 @@ class Mangafox : ParsedOnlineSource() {
         val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
         (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
             when (filter) {
-                is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
+                is Status -> url.addQueryParameter(filter.id, filter.state.toString())
+                is GenreList -> {
+                    filter.state.forEach { genre ->
+                        url.addQueryParameter(genre.id, genre.state.toString())
+                    }
+                }
                 is TextField -> url.addQueryParameter(filter.key, filter.state)
                 is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
                 is OrderBy -> {
@@ -161,24 +166,29 @@ class Mangafox : ParsedOnlineSource() {
         }
     }
 
+    private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
     private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class Type() : Filter.List<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
-    private class OrderBy() : Filter.Sort<String>("Order by",
+    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
+    private class OrderBy : Filter.Sort("Order by",
             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
             Filter.Sort.Selection(2, false))
+    private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
 
-    // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
-    // on http://mangafox.me/search.php
     override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             Type(),
-            Genre("Completed", "is_completed"),
+            Status(),
             Filter.Separator(),
             OrderBy(),
             Filter.Separator(),
-            Filter.Header("Genres"),
+            GenreList(getGenreList())
+    )
+
+    // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
+    // on http://mangafox.me/search.php
+    private fun getGenreList() = listOf(
             Genre("Action"),
             Genre("Adult"),
             Genre("Adventure"),

+ 3 - 7
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt

@@ -162,15 +162,11 @@ class Mangahere : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
 
-    private data class ListValue(val name: String, val value: String) {
-        override fun toString(): String = name
-    }
-
-    private class Status() : Filter.TriState("Completed")
+    private class Status : Filter.TriState("Completed")
     private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class Type() : Filter.List<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
-    private class OrderBy() : Filter.Sort<String>("Order by",
+    private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
+    private class OrderBy : Filter.Sort("Order by",
             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
             Filter.Sort.Selection(2, false))
 

+ 6 - 10
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt

@@ -60,7 +60,7 @@ class Mangasee : ParsedOnlineSource() {
                     if (filter.state?.ascending != true)
                         url.addQueryParameter("sortOrder", "descending")
                 }
-                is ListField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
+                is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
                 is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                 is Genre -> when (filter.state) {
                     Filter.TriState.STATE_INCLUDE -> genres = if (genres == null) filter.name else genres + "," + filter.name
@@ -155,23 +155,19 @@ class Mangasee : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
 
-    private data class SortOption(val name: String, val keys: Array<String>, val values: Array<String>) {
-        override fun toString(): String = name
-    }
-
-    private class Sort() : Filter.Sort<String>("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
+    private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
     private class Genre(name: String) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class ListField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.List<String>(name, values, state)
+    private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
 
     // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
     // http://mangasee.co/advanced-search/
     override fun getFilterList() = FilterList(
             TextField("Years", "year"),
             TextField("Author", "author"),
-            ListField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
-            ListField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
-            ListField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
+            SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
+            SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
+            SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
             Filter.Separator(),
             Sort(),
             Filter.Separator(),

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt

@@ -164,7 +164,7 @@ class Readmangatoday : ParsedOnlineSource() {
     private class Status() : Filter.TriState("Completed")
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(name)
-    private class Type() : Filter.List<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
+    private class Type() : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
 
     // [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n')
     // http://www.readmanga.today/advanced-search

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt

@@ -237,10 +237,10 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
         }
 
         navView.onSearchClicked = {
-            val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList())
+            val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
             showProgressBar()
             adapter.clear()
-            presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items)
+            presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
         }
 
         navView.onResetClicked = {

+ 38 - 161
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt

@@ -1,29 +1,24 @@
 package eu.kanade.tachiyomi.ui.catalogue
 
 import android.content.Context
-import android.support.graphics.drawable.VectorDrawableCompat
-import android.support.v4.content.ContextCompat
-import android.support.v7.widget.RecyclerView
 import android.util.AttributeSet
-import android.view.View
 import android.view.ViewGroup
-import android.widget.*
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.davidea.flexibleadapter.items.ISectionable
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.source.model.Filter
 import eu.kanade.tachiyomi.data.source.model.FilterList
-import eu.kanade.tachiyomi.util.dpToPx
-import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.ui.catalogue.filter.*
 import eu.kanade.tachiyomi.util.inflate
-import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
 import eu.kanade.tachiyomi.widget.SimpleNavigationView
-import eu.kanade.tachiyomi.widget.SimpleTextWatcher
 import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 
 
 class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
     : SimpleNavigationView(context, attrs) {
 
-    val adapter = Adapter()
+    val adapter = FlexibleAdapter<IFlexible<*>>(null)
 
     var onSearchClicked = {}
 
@@ -32,170 +27,52 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
     init {
         recycler.adapter = adapter
         val view = inflate(R.layout.catalogue_drawer_content)
-        (view as ViewGroup).addView(recycler)
+        ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
         addView(view)
 
         search_btn.setOnClickListener { onSearchClicked() }
         reset_btn.setOnClickListener { onResetClicked() }
-    }
 
-    fun setFilters(items: FilterList) {
-        adapter.items = items
-        adapter.notifyDataSetChanged()
+        adapter.setDisplayHeadersAtStartUp(true)
+        adapter.setStickyHeaders(true)
     }
 
-    inner class Adapter : RecyclerView.Adapter<Holder>() {
-
-        var items: FilterList = FilterList()
-
-        override fun getItemCount(): Int {
-            return items.size
-        }
-
-        override fun getItemViewType(position: Int): Int {
-            return when (items[position]) {
-                is Filter.Header -> VIEW_TYPE_HEADER
-                is Filter.Separator -> VIEW_TYPE_SEPARATOR
-                is Filter.CheckBox -> VIEW_TYPE_CHECKBOX
-                is Filter.TriState -> VIEW_TYPE_MULTISTATE
-                is Filter.List<*> -> VIEW_TYPE_LIST
-                is Filter.Text -> VIEW_TYPE_TEXT
-                is Filter.Sort<*> -> VIEW_TYPE_SORT
-            }
-        }
-
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
-            return when (viewType) {
-                VIEW_TYPE_HEADER -> HeaderHolder(parent)
-                VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
-                VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, null)
-                VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, null).apply {
-                    // Adjust view with checkbox
-                    text.setPadding(4.dpToPx, 0, 0, 0)
-                    text.compoundDrawablePadding = 20.dpToPx
-                }
-                VIEW_TYPE_LIST -> SpinnerHolder(parent)
-                VIEW_TYPE_TEXT -> EditTextHolder(parent)
-                VIEW_TYPE_SORT -> SortHolder(parent)
-                else -> throw Exception("Unknown view type")
-            }
-        }
-
-        override fun onBindViewHolder(holder: Holder, position: Int) {
-            val filter = items[position]
-            when (filter) {
-                is Filter.Header -> {
-                    val view = holder.itemView as TextView
-                    view.visibility = if (filter.name.isEmpty()) View.GONE else View.VISIBLE
-                    view.text = filter.name
-                }
-                is Filter.CheckBox -> {
-                    val view = (holder as CheckboxHolder).check
-                    view.text = filter.name
-                    view.isChecked = filter.state
-                    holder.itemView.setOnClickListener {
-                        view.toggle()
-                        filter.state = view.isChecked
-                    }
-                }
-                is Filter.TriState -> {
-                    val view = (holder as MultiStateHolder).text
-                    view.text = filter.name
-
-                    fun getIcon() = VectorDrawableCompat.create(view.resources, when (filter.state) {
-                        Filter.TriState.STATE_IGNORE -> R.drawable.ic_check_box_outline_blank_24dp
-                        Filter.TriState.STATE_INCLUDE -> R.drawable.ic_check_box_24dp
-                        Filter.TriState.STATE_EXCLUDE -> R.drawable.ic_check_box_x_24dp
-                        else -> throw Exception("Unknown state")
-                    }, null)?.apply {
-                        val color = if (filter.state == Filter.TriState.STATE_INCLUDE)
-                            R.attr.colorAccent
-                        else
-                            android.R.attr.textColorSecondary
-
-                        setTint(view.context.getResourceColor(color))
-                    }
-
-                    view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-                    holder.itemView.setOnClickListener {
-                        filter.state = (filter.state + 1) % 3
-                        view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
+    fun setFilters(filters: FilterList) {
+        val items = filters.mapNotNull {
+            when (it) {
+                is Filter.Header -> HeaderItem(it)
+                is Filter.Separator -> SeparatorItem(it)
+                is Filter.CheckBox -> CheckboxItem(it)
+                is Filter.TriState -> TriStateItem(it)
+                is Filter.Text -> TextItem(it)
+                is Filter.Select<*> -> SelectItem(it)
+                is Filter.Group<*> -> {
+                    val group = GroupItem(it)
+                    val subItems = it.state.mapNotNull {
+                        when (it) {
+                            is Filter.CheckBox -> CheckboxSectionItem(it)
+                            is Filter.TriState -> TriStateSectionItem(it)
+                            is Filter.Text -> TextSectionItem(it)
+                            is Filter.Select<*> -> SelectSectionItem(it)
+                            else -> null
+                        } as? ISectionable<*, *>
                     }
+                    subItems.forEach { it.header = group }
+                    group.subItems = subItems
+                    group
                 }
-                is Filter.List<*> -> {
-                    holder as SpinnerHolder
-                    holder.text.text = filter.name + ": "
-
-                    val spinner = holder.spinner
-                    spinner.prompt = filter.name
-                    spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
-                            android.R.layout.simple_spinner_item, filter.values).apply {
-                        setDropDownViewResource(R.layout.spinner_item)
+                is Filter.Sort -> {
+                    val group = SortGroup(it)
+                    val subItems = it.values.mapNotNull {
+                        SortItem(it, group)
                     }
-                    spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
-                        filter.state = position
-                    }
-                    spinner.setSelection(filter.state)
-                }
-                is Filter.Text -> {
-                    holder as EditTextHolder
-                    holder.wrapper.visibility = if (filter.name.isEmpty()) View.GONE else View.VISIBLE
-                    holder.wrapper.hint = filter.name
-                    holder.edit.setText(filter.state)
-                    holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
-                        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                            filter.state = s.toString()
-                        }
-                    })
+                    group.subItems = subItems
+                    group
                 }
-                is Filter.Sort<*> -> {
-                    val view = (holder as SortHolder).sortView
-                    view.removeAllViews()
-                    if (!filter.name.isEmpty()) {
-                        val header = HeaderHolder(view)
-                        (header.itemView as TextView).text = filter.name
-                        view.addView(header.itemView)
-                    }
-                    val holders = Array<MultiStateHolder>(filter.values.size, { MultiStateHolder(view, null) })
-                    for ((i, rb) in holders.withIndex()) {
-                        rb.text.text = filter.values[i].toString()
-
-                        fun getIcon() = when (filter.state) {
-                            Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_down_black_32dp, null)
-                                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
-                            Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_up_black_32dp, null)
-                                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
-                            else -> ContextCompat.getDrawable(context, R.drawable.empty_drawable_32dp)
-                        }
-
-                        rb.text.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-                        rb.itemView.setOnClickListener {
-                            val pre = filter.state?.index ?: i
-                            if (pre != i) {
-                                holders[pre].text.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-                                filter.state = Filter.Sort.Selection(i, false)
-                            } else {
-                                filter.state = Filter.Sort.Selection(i, filter.state?.ascending == false)
-                            }
-                            rb.text.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-                        }
-
-                        view.addView(rb.itemView)
-                    }
-                }
-            }
-        }
-
-    }
-
-    val VIEW_TYPE_SORT = 0
-
-    private class SortHolder(parent: ViewGroup, val sortView: SortView = SortView(parent)) : Holder(sortView) {
-        class SortView(parent: ViewGroup) : LinearLayout(parent.context) {
-            init {
-                orientation = LinearLayout.VERTICAL
+                else -> null
             }
         }
+        adapter.updateDataSet(items)
     }
 
 }

+ 49 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/CheckboxItem.kt

@@ -0,0 +1,49 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+
+open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_checkbox
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        val view = holder.check
+        view.text = filter.name
+        view.isChecked = filter.state
+        holder.itemView.setOnClickListener {
+            view.toggle()
+            filter.state = view.isChecked
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is CheckboxItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        val check = itemView.findViewById(R.id.nav_view_item) as CheckBox
+    }
+}

+ 56 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/GroupItem.kt

@@ -0,0 +1,56 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.davidea.viewholders.ExpandableViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.util.setVectorCompat
+
+class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_group
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        holder.title.text = filter.name
+
+        holder.icon.setVectorCompat(if (isExpanded)
+            R.drawable.ic_expand_more_white_24dp
+        else
+            R.drawable.ic_chevron_right_white_24dp)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is GroupItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
+
+        val title = itemView.findViewById(R.id.title) as TextView
+        val icon = itemView.findViewById(R.id.expand_icon) as ImageView
+
+        override fun shouldNotifyParentOnClick(): Boolean {
+            return true
+        }
+    }
+}

+ 44 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HeaderItem.kt

@@ -0,0 +1,44 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.annotation.SuppressLint
+import android.support.design.R
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractHeaderItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.source.model.Filter
+
+class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
+
+    @SuppressLint("PrivateResource")
+    override fun getLayoutRes(): Int {
+        return R.layout.design_navigation_item_subheader
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        val view = holder.itemView as TextView
+        view.visibility = if (filter.name.isEmpty()) View.GONE else View.VISIBLE
+        view.text = filter.name
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is HeaderItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, val adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
+}

+ 48 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SectionItems.kt

@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.kanade.tachiyomi.data.source.model.Filter
+
+class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+}
+
+class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+}
+
+class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+}
+
+class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
+
+    private var head: GroupItem? = null
+
+    override fun getHeader(): GroupItem? = head
+
+    override fun setHeader(header: GroupItem?) {
+        head = header
+    }
+}

+ 58 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SelectItem.kt

@@ -0,0 +1,58 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import android.widget.TextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
+
+open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_spinner
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        holder.text.text = filter.name + ": "
+
+        val spinner = holder.spinner
+        spinner.prompt = filter.name
+        spinner.adapter = ArrayAdapter<Any>(holder.itemView.context,
+                android.R.layout.simple_spinner_item, filter.values).apply {
+            setDropDownViewResource(R.layout.spinner_item)
+        }
+        spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
+            filter.state = position
+        }
+        spinner.setSelection(filter.state)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is SelectItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        val text = itemView.findViewById(R.id.nav_view_item_text) as TextView
+        val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner
+    }
+}

+ 41 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SeparatorItem.kt

@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.annotation.SuppressLint
+import android.support.design.R
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractHeaderItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.source.model.Filter
+
+class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
+
+    @SuppressLint("PrivateResource")
+    override fun getLayoutRes(): Int {
+        return R.layout.design_navigation_item_separator
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is SeparatorItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, val adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
+}

+ 57 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortGroup.kt

@@ -0,0 +1,57 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.davidea.viewholders.ExpandableViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.util.setVectorCompat
+
+class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_sort
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        holder.title.text = filter.name
+
+        holder.icon.setVectorCompat(if (isExpanded)
+            R.drawable.ic_expand_more_white_24dp
+        else
+            R.drawable.ic_chevron_right_white_24dp)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is SortGroup) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
+
+        val title = itemView.findViewById(R.id.title) as TextView
+        val icon = itemView.findViewById(R.id.expand_icon) as ImageView
+
+        override fun shouldNotifyParentOnClick(): Boolean {
+            return true
+        }
+    }
+
+}

+ 73 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/SortItem.kt

@@ -0,0 +1,73 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.support.v4.content.ContextCompat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckedTextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractSectionableItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.util.getResourceColor
+
+class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_sort_item
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        val view = holder.text
+        view.text = name
+        val filter = group.filter
+
+        val i = filter.values.indexOf(name)
+
+        fun getIcon() = when (filter.state) {
+            Filter.Sort.Selection(i, false) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_down_black_32dp, null)
+                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
+            Filter.Sort.Selection(i, true) -> VectorDrawableCompat.create(view.resources, R.drawable.ic_keyboard_arrow_up_black_32dp, null)
+                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
+            else -> ContextCompat.getDrawable(view.context, R.drawable.empty_drawable_32dp)
+        }
+
+        view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
+        holder.itemView.setOnClickListener {
+            val pre = filter.state?.index ?: i
+            if (pre != i) {
+                filter.state = Filter.Sort.Selection(i, false)
+            } else {
+                filter.state = Filter.Sort.Selection(i, filter.state?.ascending == false)
+            }
+
+            group.subItems.forEach { adapter.notifyItemChanged(adapter.getGlobalPositionOf(it)) }
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is SortItem) {
+            return name == other.name && group == other.group
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        var result = name.hashCode()
+        result = 31 * result + group.hashCode()
+        return result
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView
+    }
+
+}

+ 53 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TextItem.kt

@@ -0,0 +1,53 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.support.design.widget.TextInputLayout
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.widget.SimpleTextWatcher
+
+open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
+
+    override fun getLayoutRes(): Int {
+        return R.layout.navigation_view_text
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        holder.wrapper.visibility = if (filter.name.isEmpty()) View.GONE else View.VISIBLE
+        holder.wrapper.hint = filter.name
+        holder.edit.setText(filter.state)
+        holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                filter.state = s.toString()
+            }
+        })
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is TextItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        val wrapper  = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout
+        val edit = itemView.findViewById(R.id.nav_view_item) as EditText
+    }
+}

+ 75 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/TriStateItem.kt

@@ -0,0 +1,75 @@
+package eu.kanade.tachiyomi.ui.catalogue.filter
+
+import android.support.design.R
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckedTextView
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.source.model.Filter
+import eu.kanade.tachiyomi.util.dpToPx
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.R as TR
+
+open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriStateItem.Holder>() {
+
+    override fun getLayoutRes(): Int {
+        return TR.layout.navigation_view_checkedtext
+    }
+
+    override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup?): Holder {
+        return Holder(inflater.inflate(layoutRes, parent, false), adapter)
+    }
+
+    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
+        val view = holder.text
+        view.text = filter.name
+
+        fun getIcon() = VectorDrawableCompat.create(view.resources, when (filter.state) {
+            Filter.TriState.STATE_IGNORE -> TR.drawable.ic_check_box_outline_blank_24dp
+            Filter.TriState.STATE_INCLUDE -> TR.drawable.ic_check_box_24dp
+            Filter.TriState.STATE_EXCLUDE -> TR.drawable.ic_check_box_x_24dp
+            else -> throw Exception("Unknown state")
+        }, null)?.apply {
+            val color = if (filter.state == Filter.TriState.STATE_INCLUDE)
+                R.attr.colorAccent
+            else
+                android.R.attr.textColorSecondary
+
+            setTint(view.context.getResourceColor(color))
+        }
+
+        view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
+        holder.itemView.setOnClickListener {
+            filter.state = (filter.state + 1) % 3
+            view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other is TriStateItem) {
+            return filter == other.filter
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        return filter.hashCode()
+    }
+
+    class Holder(view: View, val adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
+
+        val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView
+
+        init {
+            // Align with native checkbox
+            text.setPadding(4.dpToPx, 0, 0, 0)
+            text.compoundDrawablePadding = 20.dpToPx
+        }
+    }
+
+}

+ 0 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryAdapter.kt

@@ -17,7 +17,6 @@ class CategoryAdapter(private val activity: CategoryActivity) :
      * Called when item is released.
      */
     fun onItemReleased() {
-        // Update database
         activity.onItemReleased()
     }
 

+ 9 - 0
app/src/main/res/drawable/ic_chevron_right_white_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/ic_expand_more_white_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
+</vector>

+ 4 - 0
app/src/main/res/layout/catalogue_drawer_content.xml

@@ -25,4 +25,8 @@
 
     </LinearLayout>
 
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
 </LinearLayout>

+ 30 - 0
app/src/main/res/layout/navigation_view_group.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/listPreferredItemHeightSmall"
+    android:background="?colorPrimary"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
+    android:paddingRight="?attr/listPreferredItemPaddingRight"
+    android:elevation="2dp">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+        android:textColor="@color/textColorPrimaryDark"
+        tools:text="Header"/>
+
+    <ImageView
+        android:id="@+id/expand_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>

+ 30 - 0
app/src/main/res/layout/navigation_view_sort.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/listPreferredItemHeightSmall"
+    android:background="?colorPrimary"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:paddingLeft="?attr/listPreferredItemPaddingLeft"
+    android:paddingRight="?attr/listPreferredItemPaddingRight"
+    android:elevation="2dp">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
+        android:textColor="@color/textColorPrimaryDark"
+        tools:text="Header"/>
+
+    <ImageView
+        android:id="@+id/expand_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>

+ 21 - 0
app/src/main/res/layout/navigation_view_sort_item.xml

@@ -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:background="?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>