mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 22:21:52 +01:00
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:
parent
2032ba3ba3
commit
d3e9200a7f
@ -53,7 +53,7 @@ abstract class OnlineSource() : Source {
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
abstract val supportsLatest : Boolean
|
||||
abstract val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
@ -133,7 +133,7 @@ abstract class OnlineSource() : Source {
|
||||
* the current page and the next page url.
|
||||
* @param query the search query.
|
||||
*/
|
||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter<*>>): Observable<MangasPage> = client
|
||||
.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
@ -148,7 +148,7 @@ abstract class OnlineSource() : Source {
|
||||
* @param page the page object.
|
||||
* @param query the search query.
|
||||
*/
|
||||
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = searchMangaInitialUrl(query, filters)
|
||||
}
|
||||
@ -160,7 +160,7 @@ abstract class OnlineSource() : Source {
|
||||
*
|
||||
* @param query the search query.
|
||||
*/
|
||||
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
|
||||
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String
|
||||
|
||||
/**
|
||||
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
||||
@ -170,7 +170,7 @@ abstract class OnlineSource() : Source {
|
||||
* @param page the page object to be filled.
|
||||
* @param query the search query.
|
||||
*/
|
||||
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
|
||||
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>)
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga.
|
||||
@ -365,10 +365,10 @@ abstract class OnlineSource() : Source {
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
final override fun fetchImage(page: Page): Observable<Page> =
|
||||
if (page.imageUrl.isNullOrEmpty())
|
||||
fetchImageUrl(page).flatMap { getCachedImage(it) }
|
||||
else
|
||||
getCachedImage(page)
|
||||
if (page.imageUrl.isNullOrEmpty())
|
||||
fetchImageUrl(page).flatMap { getCachedImage(it) }
|
||||
else
|
||||
getCachedImage(page)
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
@ -460,10 +460,21 @@ abstract class OnlineSource() : Source {
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
|
||||
|
||||
}
|
||||
|
||||
data class Filter(val id: String, val name: String)
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
|
||||
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
|
||||
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
|
||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
|
||||
companion object {
|
||||
const val STATE_IGNORE = 0
|
||||
const val STATE_INCLUDE = 1
|
||||
const val STATE_EXCLUDE = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getFilterList(): List<Filter> = emptyList()
|
||||
open fun getFilterList(): List<Filter<*>> = emptyList()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.Html
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -107,26 +107,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1${getFilterParams(filters)}"
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = searchMangaUrl(query, filters, 1)
|
||||
|
||||
private fun getFilterParams(filters: List<Filter>): String {
|
||||
private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, page: Int): String {
|
||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||
var genres = ""
|
||||
var completed = ""
|
||||
for (filter in filters) {
|
||||
if (filter.equals(completedFilter)) completed = "&completed=c"
|
||||
else genres += ";i" + filter.id
|
||||
for (filter in if (filterStates.isEmpty()) filters else filterStates) {
|
||||
when (filter) {
|
||||
is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c")
|
||||
}
|
||||
is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) {
|
||||
genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id
|
||||
}
|
||||
is TextField -> {
|
||||
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
}
|
||||
is ListField -> {
|
||||
val sel = filter.values[filter.state].value
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
is Flag -> {
|
||||
val sel = if (filter.state) filter.valTrue else filter.valFalse
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed"
|
||||
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
||||
url.addQueryParameter("p", page.toString())
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = searchMangaInitialUrl(query, filters)
|
||||
}
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(searchMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
@ -136,7 +156,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
}
|
||||
|
||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=${page.page + 1}${getFilterParams(filters)}"
|
||||
searchMangaUrl(query, filters, page.page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,51 +324,69 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
}
|
||||
}
|
||||
|
||||
private val completedFilter = Filter("completed", "Completed")
|
||||
private data class ListValue(val name: String, val value: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private class Status() : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
|
||||
private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
|
||||
|
||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
|
||||
// }).join(',\n')
|
||||
// on https://bato.to/search
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("40", "4-Koma"),
|
||||
Filter("1", "Action"),
|
||||
Filter("2", "Adventure"),
|
||||
Filter("39", "Award Winning"),
|
||||
Filter("3", "Comedy"),
|
||||
Filter("41", "Cooking"),
|
||||
Filter("9", "Doujinshi"),
|
||||
Filter("10", "Drama"),
|
||||
Filter("12", "Ecchi"),
|
||||
Filter("13", "Fantasy"),
|
||||
Filter("15", "Gender Bender"),
|
||||
Filter("17", "Harem"),
|
||||
Filter("20", "Historical"),
|
||||
Filter("22", "Horror"),
|
||||
Filter("34", "Josei"),
|
||||
Filter("27", "Martial Arts"),
|
||||
Filter("30", "Mecha"),
|
||||
Filter("42", "Medical"),
|
||||
Filter("37", "Music"),
|
||||
Filter("4", "Mystery"),
|
||||
Filter("38", "Oneshot"),
|
||||
Filter("5", "Psychological"),
|
||||
Filter("6", "Romance"),
|
||||
Filter("7", "School Life"),
|
||||
Filter("8", "Sci-fi"),
|
||||
Filter("32", "Seinen"),
|
||||
Filter("35", "Shoujo"),
|
||||
Filter("16", "Shoujo Ai"),
|
||||
Filter("33", "Shounen"),
|
||||
Filter("19", "Shounen Ai"),
|
||||
Filter("21", "Slice of Life"),
|
||||
Filter("23", "Smut"),
|
||||
Filter("25", "Sports"),
|
||||
Filter("26", "Supernatural"),
|
||||
Filter("28", "Tragedy"),
|
||||
Filter("36", "Webtoon"),
|
||||
Filter("29", "Yaoi"),
|
||||
Filter("31", "Yuri")
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
TextField("Author", "artist_name"),
|
||||
ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
||||
Status(),
|
||||
Flag("Exclude mature", "mature", "m", ""),
|
||||
Filter.Header(""),
|
||||
ListField("Order by", "order_cond", arrayOf(ListValue("Title", "title"), ListValue("Author", "author"), ListValue("Artist", "artist"), ListValue("Rating", "rating"), ListValue("Views", "views"), ListValue("Last Update", "update")), 4),
|
||||
Flag("Ascending order", "order", "asc", "desc"),
|
||||
Filter.Header("Genres"),
|
||||
ListField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
|
||||
Genre("4-Koma", 40),
|
||||
Genre("Action", 1),
|
||||
Genre("Adventure", 2),
|
||||
Genre("Award Winning", 39),
|
||||
Genre("Comedy", 3),
|
||||
Genre("Cooking", 41),
|
||||
Genre("Doujinshi", 9),
|
||||
Genre("Drama", 10),
|
||||
Genre("Ecchi", 12),
|
||||
Genre("Fantasy", 13),
|
||||
Genre("Gender Bender", 15),
|
||||
Genre("Harem", 17),
|
||||
Genre("Historical", 20),
|
||||
Genre("Horror", 22),
|
||||
Genre("Josei", 34),
|
||||
Genre("Martial Arts", 27),
|
||||
Genre("Mecha", 30),
|
||||
Genre("Medical", 42),
|
||||
Genre("Music", 37),
|
||||
Genre("Mystery", 4),
|
||||
Genre("Oneshot", 38),
|
||||
Genre("Psychological", 5),
|
||||
Genre("Romance", 6),
|
||||
Genre("School Life", 7),
|
||||
Genre("Sci-fi", 8),
|
||||
Genre("Seinen", 32),
|
||||
Genre("Shoujo", 35),
|
||||
Genre("Shoujo Ai", 16),
|
||||
Genre("Shounen", 33),
|
||||
Genre("Shounen Ai", 19),
|
||||
Genre("Slice of Life", 21),
|
||||
Genre("Smut", 23),
|
||||
Genre("Sports", 25),
|
||||
Genre("Supernatural", 26),
|
||||
Genre("Tragedy", 28),
|
||||
Genre("Webtoon", 36),
|
||||
Genre("Yaoi", 29),
|
||||
Genre("Yuri", 31),
|
||||
Genre("[no chapters]", 44)
|
||||
)
|
||||
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
@ -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")
|
||||
)
|
||||
|
||||
}
|
@ -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")
|
||||
)
|
||||
|
||||
}
|
@ -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])
|
||||
}
|
||||
is ListField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
|
||||
is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
is Genre -> when (filter.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres = if (genres == null) filter.id else genres + "," + filter.id
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo = if (genresNo == null) filter.id else genresNo + "," + filter.id
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (genres == null) url else url + "&genre=$genres"
|
||||
if (genres != null) url.addQueryParameter("genre", genres)
|
||||
if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = searchMangaInitialUrl(query, filters)
|
||||
}
|
||||
@ -95,7 +107,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
return Pair(body, requestUrl)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(popularMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
@ -174,47 +186,67 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
|
||||
private val completedFilter = Filter("Complete", "Completed")
|
||||
private data class SortOption(val name: String, val keys: Array<String>, val values: Array<String>) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private class Sort(name: String, values: Array<SortOption>, state: Int = 0) : Filter.List<SortOption>(name, values, state)
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class ListField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.List<String>(name, values, state)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://mangasee.co/advanced-search/
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
completedFilter,
|
||||
Filter("Action", "Action"),
|
||||
Filter("Adult", "Adult"),
|
||||
Filter("Adventure", "Adventure"),
|
||||
Filter("Comedy", "Comedy"),
|
||||
Filter("Doujinshi", "Doujinshi"),
|
||||
Filter("Drama", "Drama"),
|
||||
Filter("Ecchi", "Ecchi"),
|
||||
Filter("Fantasy", "Fantasy"),
|
||||
Filter("Gender_Bender", "Gender Bender"),
|
||||
Filter("Harem", "Harem"),
|
||||
Filter("Hentai", "Hentai"),
|
||||
Filter("Historical", "Historical"),
|
||||
Filter("Horror", "Horror"),
|
||||
Filter("Josei", "Josei"),
|
||||
Filter("Lolicon", "Lolicon"),
|
||||
Filter("Martial_Arts", "Martial Arts"),
|
||||
Filter("Mature", "Mature"),
|
||||
Filter("Mecha", "Mecha"),
|
||||
Filter("Mystery", "Mystery"),
|
||||
Filter("Psychological", "Psychological"),
|
||||
Filter("Romance", "Romance"),
|
||||
Filter("School_Life", "School Life"),
|
||||
Filter("Sci-fi", "Sci-fi"),
|
||||
Filter("Seinen", "Seinen"),
|
||||
Filter("Shotacon", "Shotacon"),
|
||||
Filter("Shoujo", "Shoujo"),
|
||||
Filter("Shoujo_Ai", "Shoujo Ai"),
|
||||
Filter("Shounen", "Shounen"),
|
||||
Filter("Shounen_Ai", "Shounen Ai"),
|
||||
Filter("Slice_of_Life", "Slice of Life"),
|
||||
Filter("Smut", "Smut"),
|
||||
Filter("Sports", "Sports"),
|
||||
Filter("Supernatural", "Supernatural"),
|
||||
Filter("Tragedy", "Tragedy"),
|
||||
Filter("Yaoi", "Yaoi"),
|
||||
Filter("Yuri", "Yuri")
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
TextField("Years", "year"),
|
||||
TextField("Author", "author"),
|
||||
Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
|
||||
SortOption("Alphabetical Z-A", arrayOf("sortOrder"), arrayOf("descending")),
|
||||
SortOption("Newest", arrayOf("sortBy", "sortOrder"), arrayOf("dateUpdated", "descending")),
|
||||
SortOption("Oldest", arrayOf("sortBy"), arrayOf("dateUpdated")),
|
||||
SortOption("Most Popular", arrayOf("sortBy", "sortOrder"), arrayOf("popularity", "descending")),
|
||||
SortOption("Least Popular", arrayOf("sortBy"), arrayOf("popularity"))
|
||||
), 4),
|
||||
ListField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
ListField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
ListField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
Filter.Header("Genres"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Hentai"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
@ -45,7 +45,7 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/search/?wd=$query"
|
||||
|
||||
override fun searchMangaSelector() = ".searchresult td > div"
|
||||
|
||||
@ -70,10 +70,10 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||
|
||||
if (manga.author == "RSS")
|
||||
manga.author = null
|
||||
manga.author = null
|
||||
|
||||
if (manga.artist == "RSS")
|
||||
manga.artist = null
|
||||
manga.artist = null
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||
@ -95,11 +95,12 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
val document = response.asJsoup()
|
||||
|
||||
document.select("select#page").first().select("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||
|
||||
|
@ -26,15 +26,18 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun latestUpdatesInitialUrl() = "$baseUrl/newestch"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
|
||||
if (query.isNotEmpty()) {
|
||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
||||
} else if (filters.isNotEmpty()) {
|
||||
var genres = ""
|
||||
filters.forEach { genres = genres + it.name + '+' }
|
||||
return "$baseUrl/tags/${genres.dropLast(1)}"
|
||||
} else {
|
||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
||||
val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
|
||||
if (filt.isNotEmpty()) {
|
||||
var genres = ""
|
||||
filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' }
|
||||
return "$baseUrl/tags/${genres.dropLast(1)}"
|
||||
} else {
|
||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +73,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(searchMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
@ -78,9 +81,9 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
page.mangas.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
|
||||
searchMangaNextPageSelector().let { selector ->
|
||||
if (page.nextPageUrl.isNullOrEmpty() && filters.isEmpty()) {
|
||||
if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
|
||||
val onClick = document.select(selector).first()?.attr("onclick")
|
||||
val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)"))
|
||||
page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum
|
||||
@ -88,7 +91,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
|
||||
searchGenresNextPageSelector().let { selector ->
|
||||
if (page.nextPageUrl.isNullOrEmpty() && filters.isNotEmpty()) {
|
||||
if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) {
|
||||
val url = document.select(selector).first()?.attr("href")
|
||||
page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
|
||||
}
|
||||
@ -137,71 +140,75 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||
* return `Filter("${id}", "${id}")` }).join(',\n')
|
||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
override fun getFilterList(): List<Filter> = listOf(
|
||||
Filter("18_плюс", "18_плюс"),
|
||||
Filter("bdsm", "bdsm"),
|
||||
Filter("арт", "арт"),
|
||||
Filter("биография", "биография"),
|
||||
Filter("боевик", "боевик"),
|
||||
Filter("боевые_искусства", "боевые_искусства"),
|
||||
Filter("вампиры", "вампиры"),
|
||||
Filter("веб", "веб"),
|
||||
Filter("гарем", "гарем"),
|
||||
Filter("гендерная_интрига", "гендерная_интрига"),
|
||||
Filter("героическое_фэнтези", "героическое_фэнтези"),
|
||||
Filter("детектив", "детектив"),
|
||||
Filter("дзёсэй", "дзёсэй"),
|
||||
Filter("додзинси", "додзинси"),
|
||||
Filter("драма", "драма"),
|
||||
Filter("игра", "игра"),
|
||||
Filter("инцест", "инцест"),
|
||||
Filter("искусство", "искусство"),
|
||||
Filter("история", "история"),
|
||||
Filter("киберпанк", "киберпанк"),
|
||||
Filter("кодомо", "кодомо"),
|
||||
Filter("комедия", "комедия"),
|
||||
Filter("литРПГ", "литРПГ"),
|
||||
Filter("махо-сёдзё", "махо-сёдзё"),
|
||||
Filter("меха", "меха"),
|
||||
Filter("мистика", "мистика"),
|
||||
Filter("музыка", "музыка"),
|
||||
Filter("научная_фантастика", "научная_фантастика"),
|
||||
Filter("повседневность", "повседневность"),
|
||||
Filter("постапокалиптика", "постапокалиптика"),
|
||||
Filter("приключения", "приключения"),
|
||||
Filter("психология", "психология"),
|
||||
Filter("романтика", "романтика"),
|
||||
Filter("самурайский_боевик", "самурайский_боевик"),
|
||||
Filter("сборник", "сборник"),
|
||||
Filter("сверхъестественное", "сверхъестественное"),
|
||||
Filter("сказка", "сказка"),
|
||||
Filter("спорт", "спорт"),
|
||||
Filter("супергерои", "супергерои"),
|
||||
Filter("сэйнэн", "сэйнэн"),
|
||||
Filter("сёдзё", "сёдзё"),
|
||||
Filter("сёдзё-ай", "сёдзё-ай"),
|
||||
Filter("сёнэн", "сёнэн"),
|
||||
Filter("сёнэн-ай", "сёнэн-ай"),
|
||||
Filter("тентакли", "тентакли"),
|
||||
Filter("трагедия", "трагедия"),
|
||||
Filter("триллер", "триллер"),
|
||||
Filter("ужасы", "ужасы"),
|
||||
Filter("фантастика", "фантастика"),
|
||||
Filter("фурри", "фурри"),
|
||||
Filter("фэнтези", "фэнтези"),
|
||||
Filter("школа", "школа"),
|
||||
Filter("эротика", "эротика"),
|
||||
Filter("юри", "юри"),
|
||||
Filter("яой", "яой"),
|
||||
Filter("ёнкома", "ёнкома")
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
Genre("18 плюс"),
|
||||
Genre("bdsm"),
|
||||
Genre("арт"),
|
||||
Genre("биография"),
|
||||
Genre("боевик"),
|
||||
Genre("боевые искусства"),
|
||||
Genre("вампиры"),
|
||||
Genre("веб"),
|
||||
Genre("гарем"),
|
||||
Genre("гендерная интрига"),
|
||||
Genre("героическое фэнтези"),
|
||||
Genre("детектив"),
|
||||
Genre("дзёсэй"),
|
||||
Genre("додзинси"),
|
||||
Genre("драма"),
|
||||
Genre("игра"),
|
||||
Genre("инцест"),
|
||||
Genre("искусство"),
|
||||
Genre("история"),
|
||||
Genre("киберпанк"),
|
||||
Genre("кодомо"),
|
||||
Genre("комедия"),
|
||||
Genre("литРПГ"),
|
||||
Genre("магия"),
|
||||
Genre("махо-сёдзё"),
|
||||
Genre("меха"),
|
||||
Genre("мистика"),
|
||||
Genre("музыка"),
|
||||
Genre("научная фантастика"),
|
||||
Genre("повседневность"),
|
||||
Genre("постапокалиптика"),
|
||||
Genre("приключения"),
|
||||
Genre("психология"),
|
||||
Genre("романтика"),
|
||||
Genre("самурайский боевик"),
|
||||
Genre("сборник"),
|
||||
Genre("сверхъестественное"),
|
||||
Genre("сказка"),
|
||||
Genre("спорт"),
|
||||
Genre("супергерои"),
|
||||
Genre("сэйнэн"),
|
||||
Genre("сёдзё"),
|
||||
Genre("сёдзё-ай"),
|
||||
Genre("сёнэн"),
|
||||
Genre("сёнэн-ай"),
|
||||
Genre("тентакли"),
|
||||
Genre("трагедия"),
|
||||
Genre("триллер"),
|
||||
Genre("ужасы"),
|
||||
Genre("фантастика"),
|
||||
Genre("фурри"),
|
||||
Genre("фэнтези"),
|
||||
Genre("школа"),
|
||||
Genre("эротика"),
|
||||
Genre("юри"),
|
||||
Genre("яой"),
|
||||
Genre("ёнкома")
|
||||
)
|
||||
}
|
@ -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")
|
||||
)
|
||||
}
|
@ -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")
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||
*/
|
||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
||||
|
||||
override fun createPager(query: String, filters: List<Filter>): Pager {
|
||||
override fun createPager(query: String, filters: List<Filter<*>>): Pager {
|
||||
return LatestUpdatesPager(source)
|
||||
}
|
||||
|
||||
|
9
app/src/main/res/drawable/ic_check_box_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_check_box_24dp.xml
Normal 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>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_check_box_set.xml
Normal file
5
app/src/main/res/drawable/ic_check_box_set.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_check_box_x_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_check_box_x_24dp.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user