Browse Source

Filters with flexible adapter

len 8 years ago
parent
commit
90a99dde1f
26 changed files with 747 additions and 204 deletions
  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) {
 sealed class Filter<T>(val name: String, var state: T) {
     open class Header(name: String) : Filter<Any>(name, 0)
     open class Header(name: String) : Filter<Any>(name, 0)
     open class Separator(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 Text(name: String, state: String = "") : Filter<String>(name, state)
     abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(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) {
     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
             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) {
         : Filter<Sort.Selection?>(name, state) {
         data class Selection(val index: Int, val ascending: Boolean)
         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
 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())
     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 -> {
                 is TextField -> {
                     if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                     if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                 }
                 }
-                is ListField -> {
+                is SelectField -> {
                     val sel = filter.values[filter.state].value
                     val sel = filter.values[filter.state].value
                     if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
                     if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
                 }
                 }
@@ -290,9 +290,9 @@ class Batoto : ParsedOnlineSource(), LoginSource {
     private class Status : Filter.TriState("Completed")
     private class Status : Filter.TriState("Completed")
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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 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"),
             arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
             Filter.Sort.Selection(4, false))
             Filter.Sort.Selection(4, false))
 
 
@@ -302,14 +302,14 @@ class Batoto : ParsedOnlineSource(), LoginSource {
     // on https://bato.to/search
     // on https://bato.to/search
     override fun getFilterList() = FilterList(
     override fun getFilterList() = FilterList(
             TextField("Author", "artist_name"),
             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(),
             Status(),
             Flag("Exclude mature", "mature", "m", ""),
             Flag("Exclude mature", "mature", "m", ""),
             Filter.Separator(),
             Filter.Separator(),
             OrderBy(),
             OrderBy(),
             Filter.Separator(),
             Filter.Separator(),
             Filter.Header("Genres"),
             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("4-Koma", 40),
             Genre("Action", 1),
             Genre("Action", 1),
             Genre("Adventure", 2),
             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)
         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 ->
         (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
             when (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 TextField -> url.addQueryParameter(filter.key, filter.state)
                 is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
                 is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
                 is OrderBy -> {
                 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 Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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"),
             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
             Filter.Sort.Selection(2, false))
             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(
     override fun getFilterList() = FilterList(
             TextField("Author", "author"),
             TextField("Author", "author"),
             TextField("Artist", "artist"),
             TextField("Artist", "artist"),
             Type(),
             Type(),
-            Genre("Completed", "is_completed"),
+            Status(),
             Filter.Separator(),
             Filter.Separator(),
             OrderBy(),
             OrderBy(),
             Filter.Separator(),
             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("Action"),
             Genre("Adult"),
             Genre("Adult"),
             Genre("Adventure"),
             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")
     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 Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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"),
             arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
             Filter.Sort.Selection(2, false))
             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)
                     if (filter.state?.ascending != true)
                         url.addQueryParameter("sortOrder", "descending")
                         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 TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
                 is Genre -> when (filter.state) {
                 is Genre -> when (filter.state) {
                     Filter.TriState.STATE_INCLUDE -> genres = if (genres == null) filter.name else genres + "," + filter.name
                     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")
     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 Genre(name: String) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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')
     // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
     // http://mangasee.co/advanced-search/
     // http://mangasee.co/advanced-search/
     override fun getFilterList() = FilterList(
     override fun getFilterList() = FilterList(
             TextField("Years", "year"),
             TextField("Years", "year"),
             TextField("Author", "author"),
             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(),
             Filter.Separator(),
             Sort(),
             Sort(),
             Filter.Separator(),
             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 Status() : Filter.TriState("Completed")
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class Genre(name: String, val id: Int) : Filter.TriState(name)
     private class TextField(name: String, val key: String) : Filter.Text(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')
     // [...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
     // 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 = {
         navView.onSearchClicked = {
-            val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList())
+            val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
             showProgressBar()
             showProgressBar()
             adapter.clear()
             adapter.clear()
-            presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items)
+            presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
         }
         }
 
 
         navView.onResetClicked = {
         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
 package eu.kanade.tachiyomi.ui.catalogue
 
 
 import android.content.Context
 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.util.AttributeSet
-import android.view.View
 import android.view.ViewGroup
 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.R
 import eu.kanade.tachiyomi.data.source.model.Filter
 import eu.kanade.tachiyomi.data.source.model.Filter
 import eu.kanade.tachiyomi.data.source.model.FilterList
 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.util.inflate
-import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
 import eu.kanade.tachiyomi.widget.SimpleNavigationView
 import eu.kanade.tachiyomi.widget.SimpleNavigationView
-import eu.kanade.tachiyomi.widget.SimpleTextWatcher
 import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 
 
 
 
 class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
     : SimpleNavigationView(context, attrs) {
     : SimpleNavigationView(context, attrs) {
 
 
-    val adapter = Adapter()
+    val adapter = FlexibleAdapter<IFlexible<*>>(null)
 
 
     var onSearchClicked = {}
     var onSearchClicked = {}
 
 
@@ -32,170 +27,52 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
     init {
     init {
         recycler.adapter = adapter
         recycler.adapter = adapter
         val view = inflate(R.layout.catalogue_drawer_content)
         val view = inflate(R.layout.catalogue_drawer_content)
-        (view as ViewGroup).addView(recycler)
+        ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
         addView(view)
         addView(view)
 
 
         search_btn.setOnClickListener { onSearchClicked() }
         search_btn.setOnClickListener { onSearchClicked() }
         reset_btn.setOnClickListener { onResetClicked() }
         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.
      * Called when item is released.
      */
      */
     fun onItemReleased() {
     fun onItemReleased() {
-        // Update database
         activity.onItemReleased()
         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>
     </LinearLayout>
 
 
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
 </LinearLayout>
 </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>