Browse Source

Improve catalog search filters (#615)

* Add three state (include/exclude/ignore) search filters (works for now only on MangaFox and MangaHere)

* checkbox icons in xml format

* fix checkbox icons referencing

* fix three states filters in remaining catalogs

* use Spinner for filter with more than three states (Mangasee)

* use EditText for freetext filters (Mangasee)

* remove pngs

* Filter class/subclass

* add Filter.Header

* English catalogs
paronos 8 years ago
parent
commit
d3e9200a7f
22 changed files with 853 additions and 495 deletions
  1. 23 12
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt
  3. 4 4
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt
  4. 91 53
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt
  5. 61 55
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt
  6. 70 42
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt
  7. 69 38
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt
  8. 81 49
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt
  9. 64 53
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt
  10. 6 5
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt
  11. 76 69
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt
  12. 50 47
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt
  13. 49 46
      app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt
  14. 12 10
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt
  15. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt
  16. 9 9
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  17. 153 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/FilterAdapter.kt
  18. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
  19. 9 0
      app/src/main/res/drawable/ic_check_box_24dp.xml
  20. 9 0
      app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml
  21. 5 0
      app/src/main/res/drawable/ic_check_box_set.xml
  22. 9 0
      app/src/main/res/drawable/ic_check_box_x_24dp.xml

+ 23 - 12
app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt

@@ -53,7 +53,7 @@ abstract class OnlineSource() : Source {
     /**
      * Whether the source has support for latest updates.
      */
-    abstract val supportsLatest : Boolean
+    abstract val supportsLatest: Boolean
 
     /**
      * Headers used for requests.
@@ -133,7 +133,7 @@ abstract class OnlineSource() : Source {
      *             the current page and the next page url.
      * @param query the search query.
      */
-    open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
+    open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter<*>>): Observable<MangasPage> = client
             .newCall(searchMangaRequest(page, query, filters))
             .asObservableSuccess()
             .map { response ->
@@ -148,7 +148,7 @@ abstract class OnlineSource() : Source {
      * @param page the page object.
      * @param query the search query.
      */
-    open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
+    open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
@@ -160,7 +160,7 @@ abstract class OnlineSource() : Source {
      *
      * @param query the search query.
      */
-    abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
+    abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String
 
     /**
      * Parse the response from the site. It should add a list of manga and the absolute url to the
@@ -170,7 +170,7 @@ abstract class OnlineSource() : Source {
      * @param page the page object to be filled.
      * @param query the search query.
      */
-    abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
+    abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>)
 
     /**
      * Returns an observable containing a page with a list of latest manga.
@@ -365,10 +365,10 @@ abstract class OnlineSource() : Source {
      * @param page the page whose source image has to be downloaded.
      */
     final override fun fetchImage(page: Page): Observable<Page> =
-        if (page.imageUrl.isNullOrEmpty())
-            fetchImageUrl(page).flatMap { getCachedImage(it) }
-        else
-            getCachedImage(page)
+            if (page.imageUrl.isNullOrEmpty())
+                fetchImageUrl(page).flatMap { getCachedImage(it) }
+            else
+                getCachedImage(page)
 
     /**
      * Returns an observable with the response of the source image.
@@ -460,10 +460,21 @@ abstract class OnlineSource() : Source {
      * @param manga the manga of the chapter.
      */
     open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
-
     }
 
-    data class Filter(val id: String, val name: String)
+    sealed class Filter<T>(val name: String, var state: T) {
+        open class Header(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 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) {
+            companion object {
+                const val STATE_IGNORE = 0
+                const val STATE_INCLUDE = 1
+                const val STATE_EXCLUDE = 2
+            }
+        }
+    }
 
-    open fun getFilterList(): List<Filter> = emptyList()
+    open fun getFilterList(): List<Filter<*>> = emptyList()
 }

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

@@ -61,7 +61,7 @@ abstract class ParsedOnlineSource() : OnlineSource() {
      * @param page the page object to be filled.
      * @param query the search query.
      */
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
+    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
         val document = response.asJsoup()
         for (element in document.select(searchMangaSelector())) {
             Manga.create(id).apply {

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt

@@ -30,7 +30,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
 
     override val supportsLatest = map.latestupdates != null
 
-    override val client = when(map.client) {
+    override val client = when (map.client) {
         "cloudflare" -> network.cloudflareClient
         else -> network.client
     }
@@ -66,7 +66,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
         }
     }
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
+    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
@@ -76,9 +76,9 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
         }
     }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = map.search.url.replace("\$query", query)
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = map.search.url.replace("\$query", query)
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
+    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
         val document = response.asJsoup()
         for (element in document.select(map.search.manga_css)) {
             Manga.create(id).apply {

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

@@ -1,6 +1,5 @@
 package eu.kanade.tachiyomi.data.source.online.english
 
-import android.net.Uri
 import android.text.Html
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
@@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
 import eu.kanade.tachiyomi.util.selectText
 import okhttp3.FormBody
+import okhttp3.HttpUrl
 import okhttp3.Request
 import okhttp3.Response
 import org.jsoup.nodes.Document
@@ -107,26 +107,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
 
     override fun latestUpdatesNextPageSelector() = "#show_more_row"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1${getFilterParams(filters)}"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = searchMangaUrl(query, filters, 1)
 
-    private fun getFilterParams(filters: List<Filter>): String {
+    private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, page: Int): String {
+        val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
+        if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
         var genres = ""
-        var completed = ""
-        for (filter in filters) {
-            if (filter.equals(completedFilter)) completed = "&completed=c"
-            else genres += ";i" + filter.id
+        for (filter in if (filterStates.isEmpty()) filters else filterStates) {
+            when (filter) {
+                is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) {
+                    url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c")
+                }
+                is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) {
+                    genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id
+                }
+                is TextField -> {
+                    if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
+                }
+                is ListField -> {
+                    val sel = filter.values[filter.state].value
+                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
+                }
+                is Flag -> {
+                    val sel = if (filter.state) filter.valTrue else filter.valFalse
+                    if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
+                }
+            }
         }
-        return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed"
+        if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
+        url.addQueryParameter("p", page.toString())
+        return url.toString()
     }
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
+    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
         return GET(page.url, headers)
     }
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
+    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
         val document = response.asJsoup()
         for (element in document.select(searchMangaSelector())) {
             Manga.create(id).apply {
@@ -136,7 +156,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         }
 
         page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
-            "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=${page.page + 1}${getFilterParams(filters)}"
+            searchMangaUrl(query, filters, page.page + 1)
         }
     }
 
@@ -304,51 +324,69 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
         }
     }
 
-    private val completedFilter = Filter("completed", "Completed")
+    private data class ListValue(val name: String, val value: String) {
+        override fun toString(): String = name
+    }
+
+    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 Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
+
     // [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
-    //     const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
+    //     const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
     // }).join(',\n')
     // on https://bato.to/search
-    override fun getFilterList(): List<Filter> = listOf(
-            completedFilter,
-            Filter("40", "4-Koma"),
-            Filter("1", "Action"),
-            Filter("2", "Adventure"),
-            Filter("39", "Award Winning"),
-            Filter("3", "Comedy"),
-            Filter("41", "Cooking"),
-            Filter("9", "Doujinshi"),
-            Filter("10", "Drama"),
-            Filter("12", "Ecchi"),
-            Filter("13", "Fantasy"),
-            Filter("15", "Gender Bender"),
-            Filter("17", "Harem"),
-            Filter("20", "Historical"),
-            Filter("22", "Horror"),
-            Filter("34", "Josei"),
-            Filter("27", "Martial Arts"),
-            Filter("30", "Mecha"),
-            Filter("42", "Medical"),
-            Filter("37", "Music"),
-            Filter("4", "Mystery"),
-            Filter("38", "Oneshot"),
-            Filter("5", "Psychological"),
-            Filter("6", "Romance"),
-            Filter("7", "School Life"),
-            Filter("8", "Sci-fi"),
-            Filter("32", "Seinen"),
-            Filter("35", "Shoujo"),
-            Filter("16", "Shoujo Ai"),
-            Filter("33", "Shounen"),
-            Filter("19", "Shounen Ai"),
-            Filter("21", "Slice of Life"),
-            Filter("23", "Smut"),
-            Filter("25", "Sports"),
-            Filter("26", "Supernatural"),
-            Filter("28", "Tragedy"),
-            Filter("36", "Webtoon"),
-            Filter("29", "Yaoi"),
-            Filter("31", "Yuri")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            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"))),
+            Status(),
+            Flag("Exclude mature", "mature", "m", ""),
+            Filter.Header(""),
+            ListField("Order by", "order_cond", arrayOf(ListValue("Title", "title"), ListValue("Author", "author"), ListValue("Artist", "artist"), ListValue("Rating", "rating"), ListValue("Views", "views"), ListValue("Last Update", "update")), 4),
+            Flag("Ascending order", "order", "asc", "desc"),
+            Filter.Header("Genres"),
+            ListField("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),
+            Genre("Award Winning", 39),
+            Genre("Comedy", 3),
+            Genre("Cooking", 41),
+            Genre("Doujinshi", 9),
+            Genre("Drama", 10),
+            Genre("Ecchi", 12),
+            Genre("Fantasy", 13),
+            Genre("Gender Bender", 15),
+            Genre("Harem", 17),
+            Genre("Historical", 20),
+            Genre("Horror", 22),
+            Genre("Josei", 34),
+            Genre("Martial Arts", 27),
+            Genre("Mecha", 30),
+            Genre("Medical", 42),
+            Genre("Music", 37),
+            Genre("Mystery", 4),
+            Genre("Oneshot", 38),
+            Genre("Psychological", 5),
+            Genre("Romance", 6),
+            Genre("School Life", 7),
+            Genre("Sci-fi", 8),
+            Genre("Seinen", 32),
+            Genre("Shoujo", 35),
+            Genre("Shoujo Ai", 16),
+            Genre("Shounen", 33),
+            Genre("Shounen Ai", 19),
+            Genre("Slice of Life", 21),
+            Genre("Smut", 23),
+            Genre("Sports", 25),
+            Genre("Supernatural", 26),
+            Genre("Tragedy", 28),
+            Genre("Webtoon", 36),
+            Genre("Yaoi", 29),
+            Genre("Yuri", 31),
+            Genre("[no chapters]", 44)
     )
 
 }

+ 61 - 55
app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt

@@ -51,25 +51,26 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
+    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
 
         val form = FormBody.Builder().apply {
-            add("authorArtist", "")
             add("mangaName", query)
 
-            [email protected] { filter ->
-                if (filter.equals(completedFilter)) add("status", if (filter in filters) filter.id else "")
-                else add("genres", if (filter in filters) "1" else "0")
+            for (filter in if (filters.isEmpty()) [email protected] else filters) {
+                when (filter) {
+                    is Author -> add("authorArtist", filter.state)
+                    is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
+                    is Genre -> add("genres", filter.state.toString())
+                }
             }
         }
-
         return POST(page.url, headers, form.build())
     }
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/AdvanceSearch"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/AdvanceSearch"
 
     override fun searchMangaSelector() = popularMangaSelector()
 
@@ -128,54 +129,59 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document) = ""
 
-    private val completedFilter = Filter("Completed", "Completed")
-    // $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
+    private class Status() : Filter.TriState("Completed")
+    private class Author() : Filter.Text("Author")
+    private class Genre(name: String, val id: Int) : Filter.TriState(name)
+
+    // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
     // on http://kissmanga.com/AdvanceSearch
-    override fun getFilterList(): List<Filter> = listOf(
-            completedFilter,
-            Filter("0", "Action"),
-            Filter("1", "Adult"),
-            Filter("2", "Adventure"),
-            Filter("3", "Comedy"),
-            Filter("4", "Comic"),
-            Filter("5", "Cooking"),
-            Filter("6", "Doujinshi"),
-            Filter("7", "Drama"),
-            Filter("8", "Ecchi"),
-            Filter("9", "Fantasy"),
-            Filter("10", "Gender Bender"),
-            Filter("11", "Harem"),
-            Filter("12", "Historical"),
-            Filter("13", "Horror"),
-            Filter("14", "Josei"),
-            Filter("15", "Lolicon"),
-            Filter("16", "Manga"),
-            Filter("17", "Manhua"),
-            Filter("18", "Manhwa"),
-            Filter("19", "Martial Arts"),
-            Filter("20", "Mature"),
-            Filter("21", "Mecha"),
-            Filter("22", "Medical"),
-            Filter("23", "Music"),
-            Filter("24", "Mystery"),
-            Filter("25", "One shot"),
-            Filter("26", "Psychological"),
-            Filter("27", "Romance"),
-            Filter("28", "School Life"),
-            Filter("29", "Sci-fi"),
-            Filter("30", "Seinen"),
-            Filter("31", "Shotacon"),
-            Filter("32", "Shoujo"),
-            Filter("33", "Shoujo Ai"),
-            Filter("34", "Shounen"),
-            Filter("35", "Shounen Ai"),
-            Filter("36", "Slice of Life"),
-            Filter("37", "Smut"),
-            Filter("38", "Sports"),
-            Filter("39", "Supernatural"),
-            Filter("40", "Tragedy"),
-            Filter("41", "Webtoon"),
-            Filter("42", "Yaoi"),
-            Filter("43", "Yuri")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            Author(),
+            Status(),
+            Filter.Header("Genres"),
+            Genre("Action", 0),
+            Genre("Adult", 1),
+            Genre("Adventure", 2),
+            Genre("Comedy", 3),
+            Genre("Comic", 4),
+            Genre("Cooking", 5),
+            Genre("Doujinshi", 6),
+            Genre("Drama", 7),
+            Genre("Ecchi", 8),
+            Genre("Fantasy", 9),
+            Genre("Gender Bender", 10),
+            Genre("Harem", 11),
+            Genre("Historical", 12),
+            Genre("Horror", 13),
+            Genre("Josei", 14),
+            Genre("Lolicon", 15),
+            Genre("Manga", 16),
+            Genre("Manhua", 17),
+            Genre("Manhwa", 18),
+            Genre("Martial Arts", 19),
+            Genre("Mature", 20),
+            Genre("Mecha", 21),
+            Genre("Medical", 22),
+            Genre("Music", 23),
+            Genre("Mystery", 24),
+            Genre("One shot", 25),
+            Genre("Psychological", 26),
+            Genre("Romance", 27),
+            Genre("School Life", 28),
+            Genre("Sci-fi", 29),
+            Genre("Seinen", 30),
+            Genre("Shotacon", 31),
+            Genre("Shoujo", 32),
+            Genre("Shoujo Ai", 33),
+            Genre("Shounen", 34),
+            Genre("Shounen Ai", 35),
+            Genre("Slice of Life", 36),
+            Genre("Smut", 37),
+            Genre("Sports", 38),
+            Genre("Supernatural", 39),
+            Genre("Tragedy", 40),
+            Genre("Webtoon", 41),
+            Genre("Yaoi", 42),
+            Genre("Yuri", 43)
     )
 }

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

@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.HttpUrl
 import okhttp3.Response
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
@@ -45,8 +46,18 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
-            "$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+        val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
+        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            when (filter) {
+                is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
+                is TextField -> url.addQueryParameter(filter.key, filter.state)
+                is ListField -> url.addQueryParameter(filter.key, filter.values[filter.state].value)
+                is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
+            }
+        }
+        return url.toString()
+    }
 
     override fun searchMangaSelector() = "div#mangalist > ul.list > li"
 
@@ -123,49 +134,66 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
     }
 
     // Not used, overrides parent.
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {}
+    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    }
 
     override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
 
-    // $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
-    // on http://kissmanga.com/AdvanceSearch
-    override fun getFilterList(): List<Filter> = listOf(
-            Filter("is_completed", "Completed"),
-            Filter("genres[Action]", "Action"),
-            Filter("genres[Adult]", "Adult"),
-            Filter("genres[Adventure]", "Adventure"),
-            Filter("genres[Comedy]", "Comedy"),
-            Filter("genres[Doujinshi]", "Doujinshi"),
-            Filter("genres[Drama]", "Drama"),
-            Filter("genres[Ecchi]", "Ecchi"),
-            Filter("genres[Fantasy]", "Fantasy"),
-            Filter("genres[Gender Bender]", "Gender Bender"),
-            Filter("genres[Harem]", "Harem"),
-            Filter("genres[Historical]", "Historical"),
-            Filter("genres[Horror]", "Horror"),
-            Filter("genres[Josei]", "Josei"),
-            Filter("genres[Martial Arts]", "Martial Arts"),
-            Filter("genres[Mature]", "Mature"),
-            Filter("genres[Mecha]", "Mecha"),
-            Filter("genres[Mystery]", "Mystery"),
-            Filter("genres[One Shot]", "One Shot"),
-            Filter("genres[Psychological]", "Psychological"),
-            Filter("genres[Romance]", "Romance"),
-            Filter("genres[School Life]", "School Life"),
-            Filter("genres[Sci-fi]", "Sci-fi"),
-            Filter("genres[Seinen]", "Seinen"),
-            Filter("genres[Shoujo]", "Shoujo"),
-            Filter("genres[Shoujo Ai]", "Shoujo Ai"),
-            Filter("genres[Shounen]", "Shounen"),
-            Filter("genres[Shounen Ai]", "Shounen Ai"),
-            Filter("genres[Slice of Life]", "Slice of Life"),
-            Filter("genres[Smut]", "Smut"),
-            Filter("genres[Sports]", "Sports"),
-            Filter("genres[Supernatural]", "Supernatural"),
-            Filter("genres[Tragedy]", "Tragedy"),
-            Filter("genres[Webtoons]", "Webtoons"),
-            Filter("genres[Yaoi]", "Yaoi"),
-            Filter("genres[Yuri]", "Yuri")
+    private data class ListValue(val name: String, val value: String) {
+        override fun toString(): String = 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 ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
+    private class Order() : Filter.CheckBox("Ascending order")
+
+    // $('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(): List<Filter<*>> = listOf(
+            TextField("Author", "author"),
+            TextField("Artist", "artist"),
+            ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),
+            Genre("Completed", "is_completed"),
+            Filter.Header(""),
+            ListField("Order by", "sort", arrayOf(ListValue("Series name", "name"), ListValue("Rating", "rating"), ListValue("Views", "views"), ListValue("Total chapters", "total_chapters"), ListValue("Last chapter", "last_chapter_time")), 2),
+            Order(),
+            Filter.Header("Genres"),
+            Genre("Action"),
+            Genre("Adult"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            Genre("One Shot"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Smut"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Webtoons"),
+            Genre("Yaoi"),
+            Genre("Yuri")
     )
 
 }

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

@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
+import okhttp3.HttpUrl
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 import java.text.ParseException
@@ -47,7 +48,20 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search.php?name=$query&page=1&sort=views&order=za&${filters.map { it.id + "=1" }.joinToString("&")}&advopts=1"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+        val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
+        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            when (filter) {
+                is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
+                is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
+                is TextField -> url.addQueryParameter(filter.key, filter.state)
+                is ListField -> url.addQueryParameter(filter.key, filter.values[filter.state].value)
+                is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
+            }
+        }
+        return url.toString()
+    }
+
 
     override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
 
@@ -82,12 +96,12 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
         val urlElement = parentEl.select("a").first()
 
-        var volume = parentEl.select("span.mr6")?.first()?.text()?.trim()?:""
+        var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
         if (volume.length > 0) {
             volume = " - " + volume
         }
 
-        var title = parentEl?.textNodes()?.last()?.text()?.trim()?:""
+        var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
         if (title.length > 0) {
             title = " - " + title
         }
@@ -131,42 +145,59 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
 
-    // [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
+    private data class ListValue(val name: String, val value: String) {
+        override fun toString(): String = name
+    }
+
+    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 ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
+    private class Order() : Filter.CheckBox("Ascending order")
+
+    // [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
     // http://www.mangahere.co/advsearch.htm
-    override fun getFilterList(): List<Filter> = listOf(
-            Filter("is_completed", "Completed"),
-            Filter("genres[Action]", "Action"),
-            Filter("genres[Adventure]", "Adventure"),
-            Filter("genres[Comedy]", "Comedy"),
-            Filter("genres[Doujinshi]", "Doujinshi"),
-            Filter("genres[Drama]", "Drama"),
-            Filter("genres[Ecchi]", "Ecchi"),
-            Filter("genres[Fantasy]", "Fantasy"),
-            Filter("genres[Gender Bender]", "Gender Bender"),
-            Filter("genres[Harem]", "Harem"),
-            Filter("genres[Historical]", "Historical"),
-            Filter("genres[Horror]", "Horror"),
-            Filter("genres[Josei]", "Josei"),
-            Filter("genres[Martial Arts]", "Martial Arts"),
-            Filter("genres[Mature]", "Mature"),
-            Filter("genres[Mecha]", "Mecha"),
-            Filter("genres[Mystery]", "Mystery"),
-            Filter("genres[One Shot]", "One Shot"),
-            Filter("genres[Psychological]", "Psychological"),
-            Filter("genres[Romance]", "Romance"),
-            Filter("genres[School Life]", "School Life"),
-            Filter("genres[Sci-fi]", "Sci-fi"),
-            Filter("genres[Seinen]", "Seinen"),
-            Filter("genres[Shoujo]", "Shoujo"),
-            Filter("genres[Shoujo Ai]", "Shoujo Ai"),
-            Filter("genres[Shounen]", "Shounen"),
-            Filter("genres[Shounen Ai]", "Shounen Ai"),
-            Filter("genres[Slice of Life]", "Slice of Life"),
-            Filter("genres[Sports]", "Sports"),
-            Filter("genres[Supernatural]", "Supernatural"),
-            Filter("genres[Tragedy]", "Tragedy"),
-            Filter("genres[Yaoi]", "Yaoi"),
-            Filter("genres[Yuri]", "Yuri")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            TextField("Author", "author"),
+            TextField("Artist", "artist"),
+            ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),
+            Status(),
+            Filter.Header(""),
+            ListField("Order by", "sort", arrayOf(ListValue("Series name", "name"), ListValue("Rating", "rating"), ListValue("Views", "views"), ListValue("Total chapters", "total_chapters"), ListValue("Last chapter", "last_chapter_time")), 2),
+            Order(),
+            Filter.Header("Genres"),
+            Genre("Action"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            Genre("One Shot"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Yaoi"),
+            Genre("Yuri")
     )
 
 }

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

@@ -30,7 +30,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
 
     private val indexPattern = Pattern.compile("-index-(.*?)-")
 
-    override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending"
+    override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1"
 
     override fun popularMangaSelector() = "div.requested > div.row"
 
@@ -64,20 +64,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
     // Not used, overrides parent.
     override fun popularMangaNextPageSelector() = ""
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
-        var url = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
+        val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
+        if (!query.isEmpty()) url.addQueryParameter("keyword", query)
         var genres: String? = null
-        for (filter in filters) {
-            if (filter.equals(completedFilter)) url += "&status=Complete"
-            else if (genres == null) genres = filter.id
-            else genres += "," + filter.id
+        var genresNo: String? = null
+        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            when (filter) {
+                is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
+                    url.addQueryParameter(s, filter.values[filter.state].values[i])
+                }
+                is ListField -> 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.id else genres + "," + filter.id
+                    Filter.TriState.STATE_EXCLUDE -> genresNo = if (genresNo == null) filter.id else genresNo + "," + filter.id
+                }
+            }
         }
-        return if (genres == null) url else url + "&genre=$genres"
+        if (genres != null) url.addQueryParameter("genre", genres)
+        if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
+        return url.toString()
     }
 
     override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
+    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
@@ -95,7 +107,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
         return Pair(body, requestUrl)
     }
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
+    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
         val document = response.asJsoup()
         for (element in document.select(popularMangaSelector())) {
             Manga.create(id).apply {
@@ -174,47 +186,67 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
 
-    private val completedFilter = Filter("Complete", "Completed")
+    private data class SortOption(val name: String, val keys: Array<String>, val values: Array<String>) {
+        override fun toString(): String = name
+    }
+
+    private class Sort(name: String, values: Array<SortOption>, state: Int = 0) : Filter.List<SortOption>(name, values, state)
+    private class Genre(name: String, val id: String = name.replace(' ', '_')) : 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)
+
     // [...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(): List<Filter> = listOf(
-            completedFilter,
-            Filter("Action", "Action"),
-            Filter("Adult", "Adult"),
-            Filter("Adventure", "Adventure"),
-            Filter("Comedy", "Comedy"),
-            Filter("Doujinshi", "Doujinshi"),
-            Filter("Drama", "Drama"),
-            Filter("Ecchi", "Ecchi"),
-            Filter("Fantasy", "Fantasy"),
-            Filter("Gender_Bender", "Gender Bender"),
-            Filter("Harem", "Harem"),
-            Filter("Hentai", "Hentai"),
-            Filter("Historical", "Historical"),
-            Filter("Horror", "Horror"),
-            Filter("Josei", "Josei"),
-            Filter("Lolicon", "Lolicon"),
-            Filter("Martial_Arts", "Martial Arts"),
-            Filter("Mature", "Mature"),
-            Filter("Mecha", "Mecha"),
-            Filter("Mystery", "Mystery"),
-            Filter("Psychological", "Psychological"),
-            Filter("Romance", "Romance"),
-            Filter("School_Life", "School Life"),
-            Filter("Sci-fi", "Sci-fi"),
-            Filter("Seinen", "Seinen"),
-            Filter("Shotacon", "Shotacon"),
-            Filter("Shoujo", "Shoujo"),
-            Filter("Shoujo_Ai", "Shoujo Ai"),
-            Filter("Shounen", "Shounen"),
-            Filter("Shounen_Ai", "Shounen Ai"),
-            Filter("Slice_of_Life", "Slice of Life"),
-            Filter("Smut", "Smut"),
-            Filter("Sports", "Sports"),
-            Filter("Supernatural", "Supernatural"),
-            Filter("Tragedy", "Tragedy"),
-            Filter("Yaoi", "Yaoi"),
-            Filter("Yuri", "Yuri")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            TextField("Years", "year"),
+            TextField("Author", "author"),
+            Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
+                    SortOption("Alphabetical Z-A", arrayOf("sortOrder"), arrayOf("descending")),
+                    SortOption("Newest", arrayOf("sortBy", "sortOrder"), arrayOf("dateUpdated", "descending")),
+                    SortOption("Oldest", arrayOf("sortBy"), arrayOf("dateUpdated")),
+                    SortOption("Most Popular", arrayOf("sortBy", "sortOrder"), arrayOf("popularity", "descending")),
+                    SortOption("Least Popular", arrayOf("sortBy"), arrayOf("popularity"))
+            ), 4),
+            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")),
+            Filter.Header("Genres"),
+            Genre("Action"),
+            Genre("Adult"),
+            Genre("Adventure"),
+            Genre("Comedy"),
+            Genre("Doujinshi"),
+            Genre("Drama"),
+            Genre("Ecchi"),
+            Genre("Fantasy"),
+            Genre("Gender Bender"),
+            Genre("Harem"),
+            Genre("Hentai"),
+            Genre("Historical"),
+            Genre("Horror"),
+            Genre("Josei"),
+            Genre("Lolicon"),
+            Genre("Martial Arts"),
+            Genre("Mature"),
+            Genre("Mecha"),
+            Genre("Mystery"),
+            Genre("Psychological"),
+            Genre("Romance"),
+            Genre("School Life"),
+            Genre("Sci-fi"),
+            Genre("Seinen"),
+            Genre("Shotacon"),
+            Genre("Shoujo"),
+            Genre("Shoujo Ai"),
+            Genre("Shounen"),
+            Genre("Shounen Ai"),
+            Genre("Slice of Life"),
+            Genre("Smut"),
+            Genre("Sports"),
+            Genre("Supernatural"),
+            Genre("Tragedy"),
+            Genre("Yaoi"),
+            Genre("Yuri")
     )
 
     override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"

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

@@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.network.POST
 import eu.kanade.tachiyomi.data.source.model.MangasPage
 import eu.kanade.tachiyomi.data.source.model.Page
-import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
 import okhttp3.Headers
 import okhttp3.OkHttpClient
@@ -57,25 +56,29 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesNextPageSelector(): String = "div.hot-manga > ul.pagination > li > a:contains(»)"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
             "$baseUrl/service/advanced_search"
 
 
-    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): Request {
+    override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
         if (page.page == 1) {
             page.url = searchMangaInitialUrl(query, filters)
         }
 
         val builder = okhttp3.FormBody.Builder()
         builder.add("manga-name", query)
-        builder.add("type", "all")
-        var status = "both"
-        for (filter in filters) {
-            if (filter.equals(completedFilter)) status = filter.id
-            else builder.add("include[]", filter.id)
+        for (filter in if (filters.isEmpty()) [email protected] else filters) {
+            when (filter) {
+                is TextField -> builder.add(filter.key, filter.state)
+                is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
+                is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
+                is Genre -> when (filter.state) {
+                    Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
+                    Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", filter.id.toString())
+
+                }
+            }
         }
-        builder.add("status", status)
-
         return POST(page.url, headers, builder.build())
     }
 
@@ -118,16 +121,16 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
     }
 
     private fun parseChapterDate(date: String): Long {
-        val dateWords : List<String> = date.split(" ")
+        val dateWords: List<String> = date.split(" ")
 
         if (dateWords.size == 3) {
             val timeAgo = Integer.parseInt(dateWords[0])
-            var date : Calendar = Calendar.getInstance()
+            var date: Calendar = Calendar.getInstance()
 
             if (dateWords[1].contains("Minute")) {
-                date.add(Calendar.MINUTE, - timeAgo)
+                date.add(Calendar.MINUTE, -timeAgo)
             } else if (dateWords[1].contains("Hour")) {
-                date.add(Calendar.HOUR_OF_DAY, - timeAgo)
+                date.add(Calendar.HOUR_OF_DAY, -timeAgo)
             } else if (dateWords[1].contains("Day")) {
                 date.add(Calendar.DAY_OF_YEAR, -timeAgo)
             } else if (dateWords[1].contains("Week")) {
@@ -153,45 +156,53 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
 
     override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
 
-    private val completedFilter = Filter("completed", "Completed")
-    // [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
+    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"))
+
+    // [...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
-    override fun getFilterList(): List<Filter> = listOf(
-            completedFilter,
-            Filter("2", "Action"),
-            Filter("4", "Adventure"),
-            Filter("5", "Comedy"),
-            Filter("6", "Doujinshi"),
-            Filter("7", "Drama"),
-            Filter("8", "Ecchi"),
-            Filter("9", "Fantasy"),
-            Filter("10", "Gender Bender"),
-            Filter("11", "Harem"),
-            Filter("12", "Historical"),
-            Filter("13", "Horror"),
-            Filter("14", "Josei"),
-            Filter("15", "Lolicon"),
-            Filter("16", "Martial Arts"),
-            Filter("17", "Mature"),
-            Filter("18", "Mecha"),
-            Filter("19", "Mystery"),
-            Filter("20", "One shot"),
-            Filter("21", "Psychological"),
-            Filter("22", "Romance"),
-            Filter("23", "School Life"),
-            Filter("24", "Sci-fi"),
-            Filter("25", "Seinen"),
-            Filter("26", "Shotacon"),
-            Filter("27", "Shoujo"),
-            Filter("28", "Shoujo Ai"),
-            Filter("29", "Shounen"),
-            Filter("30", "Shounen Ai"),
-            Filter("31", "Slice of Life"),
-            Filter("32", "Smut"),
-            Filter("33", "Sports"),
-            Filter("34", "Supernatural"),
-            Filter("35", "Tragedy"),
-            Filter("36", "Yaoi"),
-            Filter("37", "Yuri")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            TextField("Author", "author-name"),
+            TextField("Artist", "artist-name"),
+            Type(),
+            Status(),
+            Filter.Header("Genres"),
+            Genre("Action", 2),
+            Genre("Adventure", 4),
+            Genre("Comedy", 5),
+            Genre("Doujinshi", 6),
+            Genre("Drama", 7),
+            Genre("Ecchi", 8),
+            Genre("Fantasy", 9),
+            Genre("Gender Bender", 10),
+            Genre("Harem", 11),
+            Genre("Historical", 12),
+            Genre("Horror", 13),
+            Genre("Josei", 14),
+            Genre("Lolicon", 15),
+            Genre("Martial Arts", 16),
+            Genre("Mature", 17),
+            Genre("Mecha", 18),
+            Genre("Mystery", 19),
+            Genre("One shot", 20),
+            Genre("Psychological", 21),
+            Genre("Romance", 22),
+            Genre("School Life", 23),
+            Genre("Sci-fi", 24),
+            Genre("Seinen", 25),
+            Genre("Shotacon", 26),
+            Genre("Shoujo", 27),
+            Genre("Shoujo Ai", 28),
+            Genre("Shounen", 29),
+            Genre("Shounen Ai", 30),
+            Genre("Slice of Life", 31),
+            Genre("Smut", 32),
+            Genre("Sports", 33),
+            Genre("Supernatural", 34),
+            Genre("Tragedy", 35),
+            Genre("Yaoi", 36),
+            Genre("Yuri", 37)
     )
 }

+ 6 - 5
app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt

@@ -45,7 +45,7 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesNextPageSelector() = null
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/search/?wd=$query"
 
     override fun searchMangaSelector() = ".searchresult td > div"
 
@@ -70,10 +70,10 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
         manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
 
         if (manga.author == "RSS")
-                manga.author = null
+            manga.author = null
 
         if (manga.artist == "RSS")
-                manga.artist = null
+            manga.artist = null
     }
 
     override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
@@ -95,11 +95,12 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
         val document = response.asJsoup()
 
         document.select("select#page").first().select("option").forEach {
-                pages.add(Page(pages.size, it.attr("value")))
+            pages.add(Page(pages.size, it.attr("value")))
         }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) {}
+    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    }
 
     override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
 

+ 76 - 69
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt

@@ -26,15 +26,18 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesInitialUrl() = "$baseUrl/newestch"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
         if (query.isNotEmpty()) {
             return "$baseUrl/?do=search&subaction=search&story=$query"
-        } else if (filters.isNotEmpty()) {
-            var genres = ""
-            filters.forEach { genres = genres + it.name + '+' }
-            return "$baseUrl/tags/${genres.dropLast(1)}"
         } else {
-            return "$baseUrl/?do=search&subaction=search&story=$query"
+            val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
+            if (filt.isNotEmpty()) {
+                var genres = ""
+                filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' }
+                return "$baseUrl/tags/${genres.dropLast(1)}"
+            } else {
+                return "$baseUrl/?do=search&subaction=search&story=$query"
+            }
         }
     }
 
@@ -70,7 +73,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
 
     private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
 
-    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
+    override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
         val document = response.asJsoup()
         for (element in document.select(searchMangaSelector())) {
             Manga.create(id).apply {
@@ -78,9 +81,9 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
                 page.mangas.add(this)
             }
         }
-
+        val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
         searchMangaNextPageSelector().let { selector ->
-            if (page.nextPageUrl.isNullOrEmpty() && filters.isEmpty()) {
+            if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
                 val onClick = document.select(selector).first()?.attr("onclick")
                 val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)"))
                 page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum
@@ -88,7 +91,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
         }
 
         searchGenresNextPageSelector().let { selector ->
-            if (page.nextPageUrl.isNullOrEmpty() && filters.isNotEmpty()) {
+            if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) {
                 val url = document.select(selector).first()?.attr("href")
                 page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
             }
@@ -137,71 +140,75 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
         pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) { }
+    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    }
 
     override fun imageUrlParse(document: Document) = ""
 
+    private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
+
     /* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
     *  { const link=el.getAttribute('href');const id=link.substr(6,link.length);
-    *  return `Filter("${id}", "${id}")` }).join(',\n')
+    *  return `Genre("${id.replace("_", " ")}")` }).join(',\n')
     *  on http://mangachan.me/
     */
-    override fun getFilterList(): List<Filter> = listOf(
-            Filter("18_плюс", "18_плюс"),
-            Filter("bdsm", "bdsm"),
-            Filter("арт", "арт"),
-            Filter("биография", "биография"),
-            Filter("боевик", "боевик"),
-            Filter("боевые_искусства", "боевые_искусства"),
-            Filter("вампиры", "вампиры"),
-            Filter("веб", "веб"),
-            Filter("гарем", "гарем"),
-            Filter("гендерная_интрига", "гендерная_интрига"),
-            Filter("героическое_фэнтези", "героическое_фэнтези"),
-            Filter("детектив", "детектив"),
-            Filter("дзёсэй", "дзёсэй"),
-            Filter("додзинси", "додзинси"),
-            Filter("драма", "драма"),
-            Filter("игра", "игра"),
-            Filter("инцест", "инцест"),
-            Filter("искусство", "искусство"),
-            Filter("история", "история"),
-            Filter("киберпанк", "киберпанк"),
-            Filter("кодомо", "кодомо"),
-            Filter("комедия", "комедия"),
-            Filter("литРПГ", "литРПГ"),
-            Filter("махо-сёдзё", "махо-сёдзё"),
-            Filter("меха", "меха"),
-            Filter("мистика", "мистика"),
-            Filter("музыка", "музыка"),
-            Filter("научная_фантастика", "научная_фантастика"),
-            Filter("повседневность", "повседневность"),
-            Filter("постапокалиптика", "постапокалиптика"),
-            Filter("приключения", "приключения"),
-            Filter("психология", "психология"),
-            Filter("романтика", "романтика"),
-            Filter("самурайский_боевик", "самурайский_боевик"),
-            Filter("сборник", "сборник"),
-            Filter("сверхъестественное", "сверхъестественное"),
-            Filter("сказка", "сказка"),
-            Filter("спорт", "спорт"),
-            Filter("супергерои", "супергерои"),
-            Filter("сэйнэн", "сэйнэн"),
-            Filter("сёдзё", "сёдзё"),
-            Filter("сёдзё-ай", "сёдзё-ай"),
-            Filter("сёнэн", "сёнэн"),
-            Filter("сёнэн-ай", "сёнэн-ай"),
-            Filter("тентакли", "тентакли"),
-            Filter("трагедия", "трагедия"),
-            Filter("триллер", "триллер"),
-            Filter("ужасы", "ужасы"),
-            Filter("фантастика", "фантастика"),
-            Filter("фурри", "фурри"),
-            Filter("фэнтези", "фэнтези"),
-            Filter("школа", "школа"),
-            Filter("эротика", "эротика"),
-            Filter("юри", "юри"),
-            Filter("яой", "яой"),
-            Filter("ёнкома", "ёнкома")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            Genre("18 плюс"),
+            Genre("bdsm"),
+            Genre("арт"),
+            Genre("биография"),
+            Genre("боевик"),
+            Genre("боевые искусства"),
+            Genre("вампиры"),
+            Genre("веб"),
+            Genre("гарем"),
+            Genre("гендерная интрига"),
+            Genre("героическое фэнтези"),
+            Genre("детектив"),
+            Genre("дзёсэй"),
+            Genre("додзинси"),
+            Genre("драма"),
+            Genre("игра"),
+            Genre("инцест"),
+            Genre("искусство"),
+            Genre("история"),
+            Genre("киберпанк"),
+            Genre("кодомо"),
+            Genre("комедия"),
+            Genre("литРПГ"),
+            Genre("магия"),
+            Genre("махо-сёдзё"),
+            Genre("меха"),
+            Genre("мистика"),
+            Genre("музыка"),
+            Genre("научная фантастика"),
+            Genre("повседневность"),
+            Genre("постапокалиптика"),
+            Genre("приключения"),
+            Genre("психология"),
+            Genre("романтика"),
+            Genre("самурайский боевик"),
+            Genre("сборник"),
+            Genre("сверхъестественное"),
+            Genre("сказка"),
+            Genre("спорт"),
+            Genre("супергерои"),
+            Genre("сэйнэн"),
+            Genre("сёдзё"),
+            Genre("сёдзё-ай"),
+            Genre("сёнэн"),
+            Genre("сёнэн-ай"),
+            Genre("тентакли"),
+            Genre("трагедия"),
+            Genre("триллер"),
+            Genre("ужасы"),
+            Genre("фантастика"),
+            Genre("фурри"),
+            Genre("фэнтези"),
+            Genre("школа"),
+            Genre("эротика"),
+            Genre("юри"),
+            Genre("яой"),
+            Genre("ёнкома")
     )
 }

+ 50 - 47
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt

@@ -25,8 +25,8 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
-            "$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
+            "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
 
     override fun popularMangaSelector() = "div.desc"
 
@@ -107,57 +107,60 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
         }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) { }
+    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    }
 
     override fun imageUrlParse(document: Document) = ""
 
+    private class Genre(name: String, val id: String) : Filter.TriState(name)
+
     /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
     *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
-    *  return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
+    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://mintmanga.com/search
     */
-    override fun getFilterList(): List<Filter> = listOf(
-            Filter("el_2220", "арт"),
-            Filter("el_1353", "бара"),
-            Filter("el_1346", "боевик"),
-            Filter("el_1334", "боевые искусства"),
-            Filter("el_1339", "вампиры"),
-            Filter("el_1333", "гарем"),
-            Filter("el_1347", "гендерная интрига"),
-            Filter("el_1337", "героическое фэнтези"),
-            Filter("el_1343", "детектив"),
-            Filter("el_1349", "дзёсэй"),
-            Filter("el_1332", "додзинси"),
-            Filter("el_1310", "драма"),
-            Filter("el_5229", "игра"),
-            Filter("el_1311", "история"),
-            Filter("el_1351", "киберпанк"),
-            Filter("el_1328", "комедия"),
-            Filter("el_1318", "меха"),
-            Filter("el_1324", "мистика"),
-            Filter("el_1325", "научная фантастика"),
-            Filter("el_1327", "повседневность"),
-            Filter("el_1342", "постапокалиптика"),
-            Filter("el_1322", "приключения"),
-            Filter("el_1335", "психология"),
-            Filter("el_1313", "романтика"),
-            Filter("el_1316", "самурайский боевик"),
-            Filter("el_1350", "сверхъестественное"),
-            Filter("el_1314", "сёдзё"),
-            Filter("el_1320", "сёдзё-ай"),
-            Filter("el_1326", "сёнэн"),
-            Filter("el_1330", "сёнэн-ай"),
-            Filter("el_1321", "спорт"),
-            Filter("el_1329", "сэйнэн"),
-            Filter("el_1344", "трагедия"),
-            Filter("el_1341", "триллер"),
-            Filter("el_1317", "ужасы"),
-            Filter("el_1331", "фантастика"),
-            Filter("el_1323", "фэнтези"),
-            Filter("el_1319", "школа"),
-            Filter("el_1340", "эротика"),
-            Filter("el_1354", "этти"),
-            Filter("el_1315", "юри"),
-            Filter("el_1336", "яой")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            Genre("арт", "el_2220"),
+            Genre("бара", "el_1353"),
+            Genre("боевик", "el_1346"),
+            Genre("боевые искусства", "el_1334"),
+            Genre("вампиры", "el_1339"),
+            Genre("гарем", "el_1333"),
+            Genre("гендерная интрига", "el_1347"),
+            Genre("героическое фэнтези", "el_1337"),
+            Genre("детектив", "el_1343"),
+            Genre("дзёсэй", "el_1349"),
+            Genre("додзинси", "el_1332"),
+            Genre("драма", "el_1310"),
+            Genre("игра", "el_5229"),
+            Genre("история", "el_1311"),
+            Genre("киберпанк", "el_1351"),
+            Genre("комедия", "el_1328"),
+            Genre("меха", "el_1318"),
+            Genre("мистика", "el_1324"),
+            Genre("научная фантастика", "el_1325"),
+            Genre("повседневность", "el_1327"),
+            Genre("постапокалиптика", "el_1342"),
+            Genre("приключения", "el_1322"),
+            Genre("психология", "el_1335"),
+            Genre("романтика", "el_1313"),
+            Genre("самурайский боевик", "el_1316"),
+            Genre("сверхъестественное", "el_1350"),
+            Genre("сёдзё", "el_1314"),
+            Genre("сёдзё-ай", "el_1320"),
+            Genre("сёнэн", "el_1326"),
+            Genre("сёнэн-ай", "el_1330"),
+            Genre("спорт", "el_1321"),
+            Genre("сэйнэн", "el_1329"),
+            Genre("трагедия", "el_1344"),
+            Genre("триллер", "el_1341"),
+            Genre("ужасы", "el_1317"),
+            Genre("фантастика", "el_1331"),
+            Genre("фэнтези", "el_1323"),
+            Genre("школа", "el_1319"),
+            Genre("эротика", "el_1340"),
+            Genre("этти", "el_1354"),
+            Genre("юри", "el_1315"),
+            Genre("яой", "el_1336")
     )
 }

+ 49 - 46
app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt

@@ -25,8 +25,8 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
 
     override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
 
-    override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
-            "$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
+    override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
+            "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
 
     override fun popularMangaSelector() = "div.desc"
 
@@ -107,56 +107,59 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
         }
     }
 
-    override fun pageListParse(document: Document, pages: MutableList<Page>) { }
+    override fun pageListParse(document: Document, pages: MutableList<Page>) {
+    }
 
     override fun imageUrlParse(document: Document) = ""
 
+    private class Genre(name: String, val id: String) : Filter.TriState(name)
+
     /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
     *  const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
-    *  return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
+    *  return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
     *  on http://readmanga.me/search
     */
-    override fun getFilterList(): List<Filter> = listOf(
-            Filter("el_5685", "арт"),
-            Filter("el_2155", "боевик"),
-            Filter("el_2143", "боевые искусства"),
-            Filter("el_2148", "вампиры"),
-            Filter("el_2142", "гарем"),
-            Filter("el_2156", "гендерная интрига"),
-            Filter("el_2146", "героическое фэнтези"),
-            Filter("el_2152", "детектив"),
-            Filter("el_2158", "дзёсэй"),
-            Filter("el_2141", "додзинси"),
-            Filter("el_2118", "драма"),
-            Filter("el_2154", "игра"),
-            Filter("el_2119", "история"),
-            Filter("el_8032", "киберпанк"),
-            Filter("el_2137", "кодомо"),
-            Filter("el_2136", "комедия"),
-            Filter("el_2147", "махо-сёдзё"),
-            Filter("el_2126", "меха"),
-            Filter("el_2132", "мистика"),
-            Filter("el_2133", "научная фантастика"),
-            Filter("el_2135", "повседневность"),
-            Filter("el_2151", "постапокалиптика"),
-            Filter("el_2130", "приключения"),
-            Filter("el_2144", "психология"),
-            Filter("el_2121", "романтика"),
-            Filter("el_2124", "самурайский боевик"),
-            Filter("el_2159", "сверхъестественное"),
-            Filter("el_2122", "сёдзё"),
-            Filter("el_2128", "сёдзё-ай"),
-            Filter("el_2134", "сёнэн"),
-            Filter("el_2139", "сёнэн-ай"),
-            Filter("el_2129", "спорт"),
-            Filter("el_2138", "сэйнэн"),
-            Filter("el_2153", "трагедия"),
-            Filter("el_2150", "триллер"),
-            Filter("el_2125", "ужасы"),
-            Filter("el_2140", "фантастика"),
-            Filter("el_2131", "фэнтези"),
-            Filter("el_2127", "школа"),
-            Filter("el_2149", "этти"),
-            Filter("el_2123", "юри")
+    override fun getFilterList(): List<Filter<*>> = listOf(
+            Genre("арт", "el_5685"),
+            Genre("боевик", "el_2155"),
+            Genre("боевые искусства", "el_2143"),
+            Genre("вампиры", "el_2148"),
+            Genre("гарем", "el_2142"),
+            Genre("гендерная интрига", "el_2156"),
+            Genre("героическое фэнтези", "el_2146"),
+            Genre("детектив", "el_2152"),
+            Genre("дзёсэй", "el_2158"),
+            Genre("додзинси", "el_2141"),
+            Genre("драма", "el_2118"),
+            Genre("игра", "el_2154"),
+            Genre("история", "el_2119"),
+            Genre("киберпанк", "el_8032"),
+            Genre("кодомо", "el_2137"),
+            Genre("комедия", "el_2136"),
+            Genre("махо-сёдзё", "el_2147"),
+            Genre("меха", "el_2126"),
+            Genre("мистика", "el_2132"),
+            Genre("научная фантастика", "el_2133"),
+            Genre("повседневность", "el_2135"),
+            Genre("постапокалиптика", "el_2151"),
+            Genre("приключения", "el_2130"),
+            Genre("психология", "el_2144"),
+            Genre("романтика", "el_2121"),
+            Genre("самурайский боевик", "el_2124"),
+            Genre("сверхъестественное", "el_2159"),
+            Genre("сёдзё", "el_2122"),
+            Genre("сёдзё-ай", "el_2128"),
+            Genre("сёнэн", "el_2134"),
+            Genre("сёнэн-ай", "el_2139"),
+            Genre("спорт", "el_2129"),
+            Genre("сэйнэн", "el_2138"),
+            Genre("трагедия", "el_2153"),
+            Genre("триллер", "el_2150"),
+            Genre("ужасы", "el_2125"),
+            Genre("фантастика", "el_2140"),
+            Genre("фэнтези", "el_2131"),
+            Genre("школа", "el_2127"),
+            Genre("этти", "el_2149"),
+            Genre("юри", "el_2123")
     )
 }

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

@@ -452,19 +452,21 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
      * Show the filter dialog for the source.
      */
     private fun showFiltersDialog() {
-        val allFilters = presenter.source.filters
-        val selectedFilters = presenter.filters
-                .map { filter -> allFilters.indexOf(filter) }
-                .toTypedArray()
-
+        val adapter = FilterAdapter(if (presenter.filters.isEmpty()) presenter.source.getFilterList() // make a copy
+        else presenter.filters)
         MaterialDialog.Builder(context)
                 .title(R.string.action_set_filter)
-                .items(allFilters.map { it.name })
-                .itemsCallbackMultiChoice(selectedFilters) { dialog, positions, text ->
-                    val newFilters = positions.map { allFilters[it] }
+                .adapter(adapter, null)
+                .onPositive() { dialog, which ->
                     showProgressBar()
-                    presenter.setSourceFilter(newFilters)
-                    true
+                    var allDefault = true
+                    for (i in 0..adapter.filters.lastIndex) {
+                        if (adapter.filters[i].state != presenter.source.filters[i].state) {
+                            allDefault = false
+                            break
+                        }
+                    }
+                    presenter.setSourceFilter(if (allDefault) emptyList() else adapter.filters)
                 }
                 .positiveText(android.R.string.ok)
                 .negativeText(android.R.string.cancel)

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt

@@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
 import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
 import rx.Observable
 
-open class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter>): Pager() {
+open class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter<*>>) : Pager() {
 
     override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
         val lastPage = lastPage

+ 9 - 9
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt

@@ -65,9 +65,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
         private set
 
     /**
-     * Active filters.
+     * Filters states.
      */
-    var filters: List<Filter> = emptyList()
+    var filters: List<Filter<*>> = emptyList()
 
     /**
      * Pager containing a list of manga results.
@@ -128,9 +128,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
      * Restarts the pager for the active source with the provided query and filters.
      *
      * @param query the query.
-     * @param filters the list of active filters (for search mode).
+     * @param filters the current state of the filters (for search mode).
      */
-    fun restartPager(query: String = this.query, filters: List<Filter> = this.filters) {
+    fun restartPager(query: String = this.query, filters: List<Filter<*>> = this.filters) {
         this.query = query
         this.filters = filters
 
@@ -362,15 +362,15 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
     }
 
     /**
-     * Set the active filters for the current source.
+     * Set the filter states for the current source.
      *
-     * @param selectedFilters a list of active filters.
+     * @param filterStates a list of active filters.
      */
-    fun setSourceFilter(selectedFilters: List<Filter>) {
-        restartPager(filters = selectedFilters)
+    fun setSourceFilter(filters: List<Filter<*>>) {
+        restartPager(filters = filters)
     }
 
-    open fun createPager(query: String, filters: List<Filter>): Pager {
+    open fun createPager(query: String, filters: List<Filter<*>>): Pager {
         return CataloguePager(source, query, filters)
     }
 

+ 153 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/FilterAdapter.kt

@@ -0,0 +1,153 @@
+package eu.kanade.tachiyomi.ui.catalogue
+
+import android.content.Context
+import android.graphics.Typeface
+import android.support.graphics.drawable.VectorDrawableCompat
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import android.widget.AdapterView.OnItemSelectedListener
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
+import android.text.TextWatcher
+import android.text.Editable
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import eu.kanade.tachiyomi.util.inflate
+
+
+class FilterAdapter(val filters: List<Filter<*>>) : RecyclerView.Adapter<FilterAdapter.ViewHolder>() {
+    private companion object {
+        const val HEADER = 0
+        const val CHECKBOX = 1
+        const val TRISTATE = 2
+        const val LIST = 3
+        const val TEXT = 4
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilterAdapter.ViewHolder {
+        return when (viewType) {
+            HEADER -> ViewHolder(SepText(parent))
+            LIST -> ViewHolder(TextSpinner(parent.context))
+            TEXT -> ViewHolder(TextEditText(parent.context))
+            else -> ViewHolder(CheckBox(parent.context))
+        }
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val filter = filters[position]
+        when (filter) {
+            is Filter.Header -> {
+                if (filter.name.isEmpty()) (holder.view as SepText).textView.visibility = View.GONE
+                else (holder.view as SepText).textView.text = filter.name
+            }
+            is Filter.CheckBox -> {
+                var checkBox = holder.view as CheckBox
+                checkBox.text = filter.name
+                checkBox.isChecked = filter.state
+                checkBox.setButtonDrawable(VectorDrawableCompat.create(checkBox.getResources(), R.drawable.ic_check_box_set, null))
+                checkBox.setOnCheckedChangeListener { buttonView, isChecked ->
+                    filter.state = isChecked
+                }
+            }
+            is Filter.TriState -> {
+                var triCheckBox = holder.view as CheckBox
+                triCheckBox.text = filter.name
+                val icons = arrayOf(VectorDrawableCompat.create(triCheckBox.getResources(), R.drawable.ic_check_box_outline_blank_24dp, null),
+                        VectorDrawableCompat.create(triCheckBox.getResources(), R.drawable.ic_check_box_24dp, null),
+                        VectorDrawableCompat.create(triCheckBox.getResources(), R.drawable.ic_check_box_x_24dp, null))
+                triCheckBox.setButtonDrawable(icons[filter.state])
+                triCheckBox.invalidate()
+                triCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
+                    filter.state = (filter.state + 1) % 3
+                    triCheckBox.setButtonDrawable(icons[filter.state])
+                    triCheckBox.invalidate()
+                }
+            }
+            is Filter.List<*> -> {
+                var txtSpin = holder.view as TextSpinner
+                if (filter.name.isEmpty()) txtSpin.textView.visibility = View.GONE
+                else txtSpin.textView.text = filter.name + ":"
+                txtSpin.spinner.adapter = ArrayAdapter<Any>(holder.view.context,
+                        android.R.layout.simple_spinner_item, filter.values)
+                txtSpin.spinner.setSelection(filter.state)
+                txtSpin.spinner.onItemSelectedListener = object : OnItemSelectedListener {
+                    override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View, pos: Int, id: Long) {
+                        filter.state = pos
+                    }
+
+                    override fun onNothingSelected(parentView: AdapterView<*>) {
+                    }
+                }
+            }
+            is Filter.Text -> {
+                var txtEdTx = holder.view as TextEditText
+                if (filter.name.isEmpty()) txtEdTx.textView.visibility = View.GONE
+                else txtEdTx.textView.text = filter.name + ":"
+                txtEdTx.editText.setText(filter.state)
+                txtEdTx.editText.addTextChangedListener(object : TextWatcher {
+                    override fun afterTextChanged(s: Editable) {
+                    }
+
+                    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+                    }
+
+                    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                        filter.state = s.toString()
+                    }
+                })
+            }
+        }
+    }
+
+    override fun getItemCount(): Int {
+        return filters.size
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return when (filters[position]) {
+            is Filter.Header -> HEADER
+            is Filter.CheckBox -> CHECKBOX
+            is Filter.TriState -> TRISTATE
+            is Filter.List<*> -> LIST
+            is Filter.Text -> TEXT
+        }
+    }
+
+    class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
+
+    private class SepText(parent: ViewGroup) : LinearLayout(parent.context) {
+        val separator: View = parent.inflate(R.layout.design_navigation_item_separator)
+        val textView: TextView = TextView(context)
+
+        init {
+            orientation = LinearLayout.VERTICAL
+            textView.setTypeface(null, Typeface.BOLD);
+            addView(separator)
+            addView(textView)
+        }
+    }
+
+    private class TextSpinner(context: Context?) : LinearLayout(context) {
+        val textView: TextView = TextView(context)
+        val spinner: Spinner = Spinner(context)
+
+        init {
+            addView(textView)
+            addView(spinner)
+        }
+    }
+
+    private class TextEditText(context: Context?) : LinearLayout(context) {
+        val textView: TextView = TextView(context)
+        val editText: EditText = EditText(context)
+
+        init {
+            addView(textView)
+            editText.setSingleLine()
+            editText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+            addView(editText)
+        }
+    }
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt

@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
  */
 class LatestUpdatesPresenter : CataloguePresenter() {
 
-    override fun createPager(query: String, filters: List<Filter>): Pager {
+    override fun createPager(query: String, filters: List<Filter<*>>): Pager {
         return LatestUpdatesPager(source)
     }
 

+ 9 - 0
app/src/main/res/drawable/ic_check_box_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="#FF000000"
+        android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/ic_check_box_outline_blank_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="#FF000000"
+        android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
+</vector>

+ 5 - 0
app/src/main/res/drawable/ic_check_box_set.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="true" android:drawable="@drawable/ic_check_box_24dp" />
+    <item android:drawable="@drawable/ic_check_box_outline_blank_24dp" />
+</selector>

+ 9 - 0
app/src/main/res/drawable/ic_check_box_x_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="#FF000000"
+        android:pathData="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z"/>
+</vector>