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

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

View File

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

View File

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

View File

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

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 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)
this@Kissmanga.filters.forEach { 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()) this@Kissmanga.filters 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)
)
}

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.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()) 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"
@ -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")
)
}

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.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()) 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)"
@ -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")
)
}

View File

@ -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()) this@Mangasee.filters else filters) {
when (filter) {
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 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"

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.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)
}
builder.add("status", status)
for (filter in if (filters.isEmpty()) this@Readmangatoday.filters 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())
}
}
}
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)
)
}

View File

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

View File

@ -26,17 +26,20 @@ 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()) {
} else {
val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
if (filt.isNotEmpty()) {
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)}"
} else {
return "$baseUrl/?do=search&subaction=search&story=$query"
}
}
}
override fun popularMangaSelector() = "div.content_row"
@ -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("ёнкома")
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

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() {
override fun createPager(query: String, filters: List<Filter>): Pager {
override fun createPager(query: String, filters: List<Filter<*>>): Pager {
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>