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
This commit is contained in:
paronos 2017-01-02 18:30:10 +01:00 committed by inorichi
parent 2032ba3ba3
commit d3e9200a7f
22 changed files with 853 additions and 495 deletions

View File

@ -133,7 +133,7 @@ abstract class OnlineSource() : Source {
* the current page and the next page url. * the current page and the next page url.
* @param query the search query. * @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)) .newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
@ -148,7 +148,7 @@ abstract class OnlineSource() : Source {
* @param page the page object. * @param page the page object.
* @param query the search query. * @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) { if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters) page.url = searchMangaInitialUrl(query, filters)
} }
@ -160,7 +160,7 @@ abstract class OnlineSource() : Source {
* *
* @param query the search query. * @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 * 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 page the page object to be filled.
* @param query the search query. * @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. * Returns an observable containing a page with a list of latest manga.
@ -460,10 +460,21 @@ abstract class OnlineSource() : Source {
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
open fun prepareNewChapter(chapter: Chapter, manga: Manga) { 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)
open fun getFilterList(): List<Filter> = emptyList() 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()
} }

View File

@ -61,7 +61,7 @@ abstract class ParsedOnlineSource() : OnlineSource() {
* @param page the page object to be filled. * @param page the page object to be filled.
* @param query the search query. * @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() val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) { for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply { Manga.create(id).apply {

View File

@ -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) { if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters) 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() val document = response.asJsoup()
for (element in document.select(map.search.manga_css)) { for (element in document.select(map.search.manga_css)) {
Manga.create(id).apply { Manga.create(id).apply {

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import android.net.Uri
import android.text.Html import android.text.Html
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.asJsoup
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document 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 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 genres = ""
var completed = "" for (filter in if (filterStates.isEmpty()) filters else filterStates) {
for (filter in filters) { when (filter) {
if (filter.equals(completedFilter)) completed = "&completed=c" is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) {
else genres += ";i" + filter.id url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c")
} }
return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed" 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)
}
}
}
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) { if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters) page.url = searchMangaInitialUrl(query, filters)
} }
return GET(page.url, headers) 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() val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) { for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply { Manga.create(id).apply {
@ -136,7 +156,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
} }
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let { 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) => { // [...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') // }).join(',\n')
// on https://bato.to/search // on https://bato.to/search
override fun getFilterList(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
completedFilter, TextField("Author", "artist_name"),
Filter("40", "4-Koma"), ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
Filter("1", "Action"), Status(),
Filter("2", "Adventure"), Flag("Exclude mature", "mature", "m", ""),
Filter("39", "Award Winning"), Filter.Header(""),
Filter("3", "Comedy"), 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),
Filter("41", "Cooking"), Flag("Ascending order", "order", "asc", "desc"),
Filter("9", "Doujinshi"), Filter.Header("Genres"),
Filter("10", "Drama"), ListField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
Filter("12", "Ecchi"), Genre("4-Koma", 40),
Filter("13", "Fantasy"), Genre("Action", 1),
Filter("15", "Gender Bender"), Genre("Adventure", 2),
Filter("17", "Harem"), Genre("Award Winning", 39),
Filter("20", "Historical"), Genre("Comedy", 3),
Filter("22", "Horror"), Genre("Cooking", 41),
Filter("34", "Josei"), Genre("Doujinshi", 9),
Filter("27", "Martial Arts"), Genre("Drama", 10),
Filter("30", "Mecha"), Genre("Ecchi", 12),
Filter("42", "Medical"), Genre("Fantasy", 13),
Filter("37", "Music"), Genre("Gender Bender", 15),
Filter("4", "Mystery"), Genre("Harem", 17),
Filter("38", "Oneshot"), Genre("Historical", 20),
Filter("5", "Psychological"), Genre("Horror", 22),
Filter("6", "Romance"), Genre("Josei", 34),
Filter("7", "School Life"), Genre("Martial Arts", 27),
Filter("8", "Sci-fi"), Genre("Mecha", 30),
Filter("32", "Seinen"), Genre("Medical", 42),
Filter("35", "Shoujo"), Genre("Music", 37),
Filter("16", "Shoujo Ai"), Genre("Mystery", 4),
Filter("33", "Shounen"), Genre("Oneshot", 38),
Filter("19", "Shounen Ai"), Genre("Psychological", 5),
Filter("21", "Slice of Life"), Genre("Romance", 6),
Filter("23", "Smut"), Genre("School Life", 7),
Filter("25", "Sports"), Genre("Sci-fi", 8),
Filter("26", "Supernatural"), Genre("Seinen", 32),
Filter("28", "Tragedy"), Genre("Shoujo", 35),
Filter("36", "Webtoon"), Genre("Shoujo Ai", 16),
Filter("29", "Yaoi"), Genre("Shounen", 33),
Filter("31", "Yuri") 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)
) )
} }

View File

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

View File

@ -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.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element 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 latestUpdatesNextPageSelector() = "a:has(span.next)"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}" 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()) this@Mangafox.filters 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" override fun searchMangaSelector() = "div#mangalist > ul.list > li"
@ -123,49 +134,66 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
} }
// Not used, overrides parent. // 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") 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') private data class ListValue(val name: String, val value: String) {
// on http://kissmanga.com/AdvanceSearch override fun toString(): String = name
override fun getFilterList(): List<Filter> = listOf( }
Filter("is_completed", "Completed"),
Filter("genres[Action]", "Action"), private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
Filter("genres[Adult]", "Adult"), private class TextField(name: String, val key: String) : Filter.Text(name)
Filter("genres[Adventure]", "Adventure"), private class ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
Filter("genres[Comedy]", "Comedy"), private class Order() : Filter.CheckBox("Ascending order")
Filter("genres[Doujinshi]", "Doujinshi"),
Filter("genres[Drama]", "Drama"), // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
Filter("genres[Ecchi]", "Ecchi"), // on http://mangafox.me/search.php
Filter("genres[Fantasy]", "Fantasy"), override fun getFilterList(): List<Filter<*>> = listOf(
Filter("genres[Gender Bender]", "Gender Bender"), TextField("Author", "author"),
Filter("genres[Harem]", "Harem"), TextField("Artist", "artist"),
Filter("genres[Historical]", "Historical"), ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),
Filter("genres[Horror]", "Horror"), Genre("Completed", "is_completed"),
Filter("genres[Josei]", "Josei"), Filter.Header(""),
Filter("genres[Martial Arts]", "Martial Arts"), 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),
Filter("genres[Mature]", "Mature"), Order(),
Filter("genres[Mecha]", "Mecha"), Filter.Header("Genres"),
Filter("genres[Mystery]", "Mystery"), Genre("Action"),
Filter("genres[One Shot]", "One Shot"), Genre("Adult"),
Filter("genres[Psychological]", "Psychological"), Genre("Adventure"),
Filter("genres[Romance]", "Romance"), Genre("Comedy"),
Filter("genres[School Life]", "School Life"), Genre("Doujinshi"),
Filter("genres[Sci-fi]", "Sci-fi"), Genre("Drama"),
Filter("genres[Seinen]", "Seinen"), Genre("Ecchi"),
Filter("genres[Shoujo]", "Shoujo"), Genre("Fantasy"),
Filter("genres[Shoujo Ai]", "Shoujo Ai"), Genre("Gender Bender"),
Filter("genres[Shounen]", "Shounen"), Genre("Harem"),
Filter("genres[Shounen Ai]", "Shounen Ai"), Genre("Historical"),
Filter("genres[Slice of Life]", "Slice of Life"), Genre("Horror"),
Filter("genres[Smut]", "Smut"), Genre("Josei"),
Filter("genres[Sports]", "Sports"), Genre("Martial Arts"),
Filter("genres[Supernatural]", "Supernatural"), Genre("Mature"),
Filter("genres[Tragedy]", "Tragedy"), Genre("Mecha"),
Filter("genres[Webtoons]", "Webtoons"), Genre("Mystery"),
Filter("genres[Yaoi]", "Yaoi"), Genre("One Shot"),
Filter("genres[Yuri]", "Yuri") 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")
) )
} }

View File

@ -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.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.HttpUrl
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.ParseException 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 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()) this@Mangahere.filters 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)" override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
@ -131,42 +145,59 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src") 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 // http://www.mangahere.co/advsearch.htm
override fun getFilterList(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
Filter("is_completed", "Completed"), TextField("Author", "author"),
Filter("genres[Action]", "Action"), TextField("Artist", "artist"),
Filter("genres[Adventure]", "Adventure"), ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),
Filter("genres[Comedy]", "Comedy"), Status(),
Filter("genres[Doujinshi]", "Doujinshi"), Filter.Header(""),
Filter("genres[Drama]", "Drama"), 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),
Filter("genres[Ecchi]", "Ecchi"), Order(),
Filter("genres[Fantasy]", "Fantasy"), Filter.Header("Genres"),
Filter("genres[Gender Bender]", "Gender Bender"), Genre("Action"),
Filter("genres[Harem]", "Harem"), Genre("Adventure"),
Filter("genres[Historical]", "Historical"), Genre("Comedy"),
Filter("genres[Horror]", "Horror"), Genre("Doujinshi"),
Filter("genres[Josei]", "Josei"), Genre("Drama"),
Filter("genres[Martial Arts]", "Martial Arts"), Genre("Ecchi"),
Filter("genres[Mature]", "Mature"), Genre("Fantasy"),
Filter("genres[Mecha]", "Mecha"), Genre("Gender Bender"),
Filter("genres[Mystery]", "Mystery"), Genre("Harem"),
Filter("genres[One Shot]", "One Shot"), Genre("Historical"),
Filter("genres[Psychological]", "Psychological"), Genre("Horror"),
Filter("genres[Romance]", "Romance"), Genre("Josei"),
Filter("genres[School Life]", "School Life"), Genre("Martial Arts"),
Filter("genres[Sci-fi]", "Sci-fi"), Genre("Mature"),
Filter("genres[Seinen]", "Seinen"), Genre("Mecha"),
Filter("genres[Shoujo]", "Shoujo"), Genre("Mystery"),
Filter("genres[Shoujo Ai]", "Shoujo Ai"), Genre("One Shot"),
Filter("genres[Shounen]", "Shounen"), Genre("Psychological"),
Filter("genres[Shounen Ai]", "Shounen Ai"), Genre("Romance"),
Filter("genres[Slice of Life]", "Slice of Life"), Genre("School Life"),
Filter("genres[Sports]", "Sports"), Genre("Sci-fi"),
Filter("genres[Supernatural]", "Supernatural"), Genre("Seinen"),
Filter("genres[Tragedy]", "Tragedy"), Genre("Shoujo"),
Filter("genres[Yaoi]", "Yaoi"), Genre("Shoujo Ai"),
Filter("genres[Yuri]", "Yuri") Genre("Shounen"),
Genre("Shounen Ai"),
Genre("Slice of Life"),
Genre("Sports"),
Genre("Supernatural"),
Genre("Tragedy"),
Genre("Yaoi"),
Genre("Yuri")
) )
} }

View File

@ -30,7 +30,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
private val indexPattern = Pattern.compile("-index-(.*?)-") 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" override fun popularMangaSelector() = "div.requested > div.row"
@ -64,20 +64,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
// Not used, overrides parent. // Not used, overrides parent.
override fun popularMangaNextPageSelector() = "" override fun popularMangaNextPageSelector() = ""
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String { override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
var url = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query" val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
var genres: String? = null var genres: String? = null
for (filter in filters) { var genresNo: String? = null
if (filter.equals(completedFilter)) url += "&status=Complete" for (filter in if (filters.isEmpty()) this@Mangasee.filters else filters) {
else if (genres == null) genres = filter.id when (filter) {
else genres += "," + filter.id is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
url.addQueryParameter(s, filter.values[filter.state].values[i])
} }
return if (genres == null) url else url + "&genre=$genres" 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
}
}
}
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 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) { if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters) page.url = searchMangaInitialUrl(query, filters)
} }
@ -95,7 +107,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
return Pair(body, requestUrl) 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() val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) { for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply { 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") 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') // [...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(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
completedFilter, TextField("Years", "year"),
Filter("Action", "Action"), TextField("Author", "author"),
Filter("Adult", "Adult"), Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
Filter("Adventure", "Adventure"), SortOption("Alphabetical Z-A", arrayOf("sortOrder"), arrayOf("descending")),
Filter("Comedy", "Comedy"), SortOption("Newest", arrayOf("sortBy", "sortOrder"), arrayOf("dateUpdated", "descending")),
Filter("Doujinshi", "Doujinshi"), SortOption("Oldest", arrayOf("sortBy"), arrayOf("dateUpdated")),
Filter("Drama", "Drama"), SortOption("Most Popular", arrayOf("sortBy", "sortOrder"), arrayOf("popularity", "descending")),
Filter("Ecchi", "Ecchi"), SortOption("Least Popular", arrayOf("sortBy"), arrayOf("popularity"))
Filter("Fantasy", "Fantasy"), ), 4),
Filter("Gender_Bender", "Gender Bender"), ListField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
Filter("Harem", "Harem"), ListField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
Filter("Hentai", "Hentai"), ListField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
Filter("Historical", "Historical"), Filter.Header("Genres"),
Filter("Horror", "Horror"), Genre("Action"),
Filter("Josei", "Josei"), Genre("Adult"),
Filter("Lolicon", "Lolicon"), Genre("Adventure"),
Filter("Martial_Arts", "Martial Arts"), Genre("Comedy"),
Filter("Mature", "Mature"), Genre("Doujinshi"),
Filter("Mecha", "Mecha"), Genre("Drama"),
Filter("Mystery", "Mystery"), Genre("Ecchi"),
Filter("Psychological", "Psychological"), Genre("Fantasy"),
Filter("Romance", "Romance"), Genre("Gender Bender"),
Filter("School_Life", "School Life"), Genre("Harem"),
Filter("Sci-fi", "Sci-fi"), Genre("Hentai"),
Filter("Seinen", "Seinen"), Genre("Historical"),
Filter("Shotacon", "Shotacon"), Genre("Horror"),
Filter("Shoujo", "Shoujo"), Genre("Josei"),
Filter("Shoujo_Ai", "Shoujo Ai"), Genre("Lolicon"),
Filter("Shounen", "Shounen"), Genre("Martial Arts"),
Filter("Shounen_Ai", "Shounen Ai"), Genre("Mature"),
Filter("Slice_of_Life", "Slice of Life"), Genre("Mecha"),
Filter("Smut", "Smut"), Genre("Mystery"),
Filter("Sports", "Sports"), Genre("Psychological"),
Filter("Supernatural", "Supernatural"), Genre("Romance"),
Filter("Tragedy", "Tragedy"), Genre("School Life"),
Filter("Yaoi", "Yaoi"), Genre("Sci-fi"),
Filter("Yuri", "Yuri") 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" override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"

View File

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page 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 eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient 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 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" "$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) { if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters) page.url = searchMangaInitialUrl(query, filters)
} }
val builder = okhttp3.FormBody.Builder() val builder = okhttp3.FormBody.Builder()
builder.add("manga-name", query) builder.add("manga-name", query)
builder.add("type", "all") for (filter in if (filters.isEmpty()) this@Readmangatoday.filters else filters) {
var status = "both" when (filter) {
for (filter in filters) { is TextField -> builder.add(filter.key, filter.state)
if (filter.equals(completedFilter)) status = filter.id is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
else builder.add("include[]", filter.id) is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
} is Genre -> when (filter.state) {
builder.add("status", status) Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", filter.id.toString())
}
}
}
return POST(page.url, headers, builder.build()) return POST(page.url, headers, builder.build())
} }
@ -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") override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
private val completedFilter = Filter("completed", "Completed") private class Status() : Filter.TriState("Completed")
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n') 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 // http://www.readmanga.today/advanced-search
override fun getFilterList(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
completedFilter, TextField("Author", "author-name"),
Filter("2", "Action"), TextField("Artist", "artist-name"),
Filter("4", "Adventure"), Type(),
Filter("5", "Comedy"), Status(),
Filter("6", "Doujinshi"), Filter.Header("Genres"),
Filter("7", "Drama"), Genre("Action", 2),
Filter("8", "Ecchi"), Genre("Adventure", 4),
Filter("9", "Fantasy"), Genre("Comedy", 5),
Filter("10", "Gender Bender"), Genre("Doujinshi", 6),
Filter("11", "Harem"), Genre("Drama", 7),
Filter("12", "Historical"), Genre("Ecchi", 8),
Filter("13", "Horror"), Genre("Fantasy", 9),
Filter("14", "Josei"), Genre("Gender Bender", 10),
Filter("15", "Lolicon"), Genre("Harem", 11),
Filter("16", "Martial Arts"), Genre("Historical", 12),
Filter("17", "Mature"), Genre("Horror", 13),
Filter("18", "Mecha"), Genre("Josei", 14),
Filter("19", "Mystery"), Genre("Lolicon", 15),
Filter("20", "One shot"), Genre("Martial Arts", 16),
Filter("21", "Psychological"), Genre("Mature", 17),
Filter("22", "Romance"), Genre("Mecha", 18),
Filter("23", "School Life"), Genre("Mystery", 19),
Filter("24", "Sci-fi"), Genre("One shot", 20),
Filter("25", "Seinen"), Genre("Psychological", 21),
Filter("26", "Shotacon"), Genre("Romance", 22),
Filter("27", "Shoujo"), Genre("School Life", 23),
Filter("28", "Shoujo Ai"), Genre("Sci-fi", 24),
Filter("29", "Shounen"), Genre("Seinen", 25),
Filter("30", "Shounen Ai"), Genre("Shotacon", 26),
Filter("31", "Slice of Life"), Genre("Shoujo", 27),
Filter("32", "Smut"), Genre("Shoujo Ai", 28),
Filter("33", "Sports"), Genre("Shounen", 29),
Filter("34", "Supernatural"), Genre("Shounen Ai", 30),
Filter("35", "Tragedy"), Genre("Slice of Life", 31),
Filter("36", "Yaoi"), Genre("Smut", 32),
Filter("37", "Yuri") Genre("Sports", 33),
Genre("Supernatural", 34),
Genre("Tragedy", 35),
Genre("Yaoi", 36),
Genre("Yuri", 37)
) )
} }

View File

@ -45,7 +45,7 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
override fun latestUpdatesNextPageSelector() = null 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" override fun searchMangaSelector() = ".searchresult td > div"
@ -99,7 +99,8 @@ class WieManga(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) = document.select("img#comicpic").first().attr("src") override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")

View File

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

View File

@ -25,8 +25,8 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated" override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}" "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
override fun popularMangaSelector() = "div.desc" 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) = "" 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) => { /* [...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); * 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 * on http://mintmanga.com/search
*/ */
override fun getFilterList(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
Filter("el_2220", "арт"), Genre("арт", "el_2220"),
Filter("el_1353", "бара"), Genre("бара", "el_1353"),
Filter("el_1346", "боевик"), Genre("боевик", "el_1346"),
Filter("el_1334", "боевые искусства"), Genre("боевые искусства", "el_1334"),
Filter("el_1339", "вампиры"), Genre("вампиры", "el_1339"),
Filter("el_1333", "гарем"), Genre("гарем", "el_1333"),
Filter("el_1347", "гендерная интрига"), Genre("гендерная интрига", "el_1347"),
Filter("el_1337", "героическое фэнтези"), Genre("героическое фэнтези", "el_1337"),
Filter("el_1343", "детектив"), Genre("детектив", "el_1343"),
Filter("el_1349", "дзёсэй"), Genre("дзёсэй", "el_1349"),
Filter("el_1332", "додзинси"), Genre("додзинси", "el_1332"),
Filter("el_1310", "драма"), Genre("драма", "el_1310"),
Filter("el_5229", "игра"), Genre("игра", "el_5229"),
Filter("el_1311", "история"), Genre("история", "el_1311"),
Filter("el_1351", "киберпанк"), Genre("киберпанк", "el_1351"),
Filter("el_1328", "комедия"), Genre("комедия", "el_1328"),
Filter("el_1318", "меха"), Genre("меха", "el_1318"),
Filter("el_1324", "мистика"), Genre("мистика", "el_1324"),
Filter("el_1325", "научная фантастика"), Genre("научная фантастика", "el_1325"),
Filter("el_1327", "повседневность"), Genre("повседневность", "el_1327"),
Filter("el_1342", "постапокалиптика"), Genre("постапокалиптика", "el_1342"),
Filter("el_1322", "приключения"), Genre("приключения", "el_1322"),
Filter("el_1335", "психология"), Genre("психология", "el_1335"),
Filter("el_1313", "романтика"), Genre("романтика", "el_1313"),
Filter("el_1316", "самурайский боевик"), Genre("самурайский боевик", "el_1316"),
Filter("el_1350", "сверхъестественное"), Genre("сверхъестественное", "el_1350"),
Filter("el_1314", "сёдзё"), Genre("сёдзё", "el_1314"),
Filter("el_1320", "сёдзё-ай"), Genre("сёдзё-ай", "el_1320"),
Filter("el_1326", "сёнэн"), Genre("сёнэн", "el_1326"),
Filter("el_1330", "сёнэн-ай"), Genre("сёнэн-ай", "el_1330"),
Filter("el_1321", "спорт"), Genre("спорт", "el_1321"),
Filter("el_1329", "сэйнэн"), Genre("сэйнэн", "el_1329"),
Filter("el_1344", "трагедия"), Genre("трагедия", "el_1344"),
Filter("el_1341", "триллер"), Genre("триллер", "el_1341"),
Filter("el_1317", "ужасы"), Genre("ужасы", "el_1317"),
Filter("el_1331", "фантастика"), Genre("фантастика", "el_1331"),
Filter("el_1323", "фэнтези"), Genre("фэнтези", "el_1323"),
Filter("el_1319", "школа"), Genre("школа", "el_1319"),
Filter("el_1340", "эротика"), Genre("эротика", "el_1340"),
Filter("el_1354", "этти"), Genre("этти", "el_1354"),
Filter("el_1315", "юри"), Genre("юри", "el_1315"),
Filter("el_1336", "яой") Genre("яой", "el_1336")
) )
} }

View File

@ -25,8 +25,8 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated" override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}" "$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
override fun popularMangaSelector() = "div.desc" 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) = "" 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) => { /* [...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); * 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 * on http://readmanga.me/search
*/ */
override fun getFilterList(): List<Filter> = listOf( override fun getFilterList(): List<Filter<*>> = listOf(
Filter("el_5685", "арт"), Genre("арт", "el_5685"),
Filter("el_2155", "боевик"), Genre("боевик", "el_2155"),
Filter("el_2143", "боевые искусства"), Genre("боевые искусства", "el_2143"),
Filter("el_2148", "вампиры"), Genre("вампиры", "el_2148"),
Filter("el_2142", "гарем"), Genre("гарем", "el_2142"),
Filter("el_2156", "гендерная интрига"), Genre("гендерная интрига", "el_2156"),
Filter("el_2146", "героическое фэнтези"), Genre("героическое фэнтези", "el_2146"),
Filter("el_2152", "детектив"), Genre("детектив", "el_2152"),
Filter("el_2158", "дзёсэй"), Genre("дзёсэй", "el_2158"),
Filter("el_2141", "додзинси"), Genre("додзинси", "el_2141"),
Filter("el_2118", "драма"), Genre("драма", "el_2118"),
Filter("el_2154", "игра"), Genre("игра", "el_2154"),
Filter("el_2119", "история"), Genre("история", "el_2119"),
Filter("el_8032", "киберпанк"), Genre("киберпанк", "el_8032"),
Filter("el_2137", "кодомо"), Genre("кодомо", "el_2137"),
Filter("el_2136", "комедия"), Genre("комедия", "el_2136"),
Filter("el_2147", "махо-сёдзё"), Genre("махо-сёдзё", "el_2147"),
Filter("el_2126", "меха"), Genre("меха", "el_2126"),
Filter("el_2132", "мистика"), Genre("мистика", "el_2132"),
Filter("el_2133", "научная фантастика"), Genre("научная фантастика", "el_2133"),
Filter("el_2135", "повседневность"), Genre("повседневность", "el_2135"),
Filter("el_2151", "постапокалиптика"), Genre("постапокалиптика", "el_2151"),
Filter("el_2130", "приключения"), Genre("приключения", "el_2130"),
Filter("el_2144", "психология"), Genre("психология", "el_2144"),
Filter("el_2121", "романтика"), Genre("романтика", "el_2121"),
Filter("el_2124", "самурайский боевик"), Genre("самурайский боевик", "el_2124"),
Filter("el_2159", "сверхъестественное"), Genre("сверхъестественное", "el_2159"),
Filter("el_2122", "сёдзё"), Genre("сёдзё", "el_2122"),
Filter("el_2128", "сёдзё-ай"), Genre("сёдзё-ай", "el_2128"),
Filter("el_2134", "сёнэн"), Genre("сёнэн", "el_2134"),
Filter("el_2139", "сёнэн-ай"), Genre("сёнэн-ай", "el_2139"),
Filter("el_2129", "спорт"), Genre("спорт", "el_2129"),
Filter("el_2138", "сэйнэн"), Genre("сэйнэн", "el_2138"),
Filter("el_2153", "трагедия"), Genre("трагедия", "el_2153"),
Filter("el_2150", "триллер"), Genre("триллер", "el_2150"),
Filter("el_2125", "ужасы"), Genre("ужасы", "el_2125"),
Filter("el_2140", "фантастика"), Genre("фантастика", "el_2140"),
Filter("el_2131", "фэнтези"), Genre("фэнтези", "el_2131"),
Filter("el_2127", "школа"), Genre("школа", "el_2127"),
Filter("el_2149", "этти"), Genre("этти", "el_2149"),
Filter("el_2123", "юри") Genre("юри", "el_2123")
) )
} }

View File

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

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
import rx.Observable 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> { override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
val lastPage = lastPage val lastPage = lastPage

View File

@ -65,9 +65,9 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private set private set
/** /**
* Active filters. * Filters states.
*/ */
var filters: List<Filter> = emptyList() var filters: List<Filter<*>> = emptyList()
/** /**
* Pager containing a list of manga results. * 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. * Restarts the pager for the active source with the provided query and filters.
* *
* @param query the query. * @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.query = query
this.filters = filters 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>) { fun setSourceFilter(filters: List<Filter<*>>) {
restartPager(filters = selectedFilters) 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) return CataloguePager(source, query, filters)
} }

View File

@ -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)
}
}
}

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>