mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 01:51:51 +01:00
Add genre filter for catalogue (#428)
* Add genre filter for catalogue * Implement genre filter for batoto * hardcode filters for sources * swtich filter id to string * reset filters when switching sources * Add filter support to mangafox * Catalogue changes * Indefinite snackbar on error, use plain subscriptions in catalogue presenter
This commit is contained in:
parent
4171e87b4b
commit
2fb3b50535
@ -47,5 +47,4 @@ interface Source {
|
|||||||
* @param page the page.
|
* @param page the page.
|
||||||
*/
|
*/
|
||||||
fun fetchImage(page: Page): Observable<Page>
|
fun fetchImage(page: Page): Observable<Page>
|
||||||
|
|
||||||
}
|
}
|
@ -58,6 +58,11 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*/
|
*/
|
||||||
val headers by lazy { headersBuilder().build() }
|
val headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genre filters.
|
||||||
|
*/
|
||||||
|
val filters by lazy { getFilterList() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
@ -126,11 +131,11 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* the current page and the next page url.
|
* the current page and the next page url.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
|
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||||
.newCall(searchMangaRequest(page, query))
|
.newCall(searchMangaRequest(page, query, filters))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
searchMangaParse(response, page, query)
|
searchMangaParse(response, page, query, filters)
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,9 +146,9 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page object.
|
* @param page the page object.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
|
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return GET(page.url, headers)
|
return GET(page.url, headers)
|
||||||
}
|
}
|
||||||
@ -153,7 +158,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*
|
*
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaInitialUrl(query: String): String
|
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
||||||
@ -163,7 +168,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page object to be filled.
|
* @param page the page object to be filled.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
|
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||||
@ -428,4 +433,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Filter(val id: String, val name: String)
|
||||||
|
|
||||||
|
open fun getFilterList(): List<Filter> = emptyList()
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param page the page object to be filled.
|
* @param page the page object to be filled.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
for (element in document.select(searchMangaSelector())) {
|
for (element in document.select(searchMangaSelector())) {
|
||||||
Manga.create(id).apply {
|
Manga.create(id).apply {
|
||||||
@ -179,5 +179,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param document the parsed document.
|
* @param document the parsed document.
|
||||||
*/
|
*/
|
||||||
abstract protected fun imageUrlParse(document: Document): String
|
abstract protected fun imageUrlParse(document: Document): String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
@ -14,6 +15,7 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -68,9 +70,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return when (map.search.method?.toLowerCase()) {
|
return when (map.search.method?.toLowerCase()) {
|
||||||
"post" -> POST(page.url, headers, map.search.createForm())
|
"post" -> POST(page.url, headers, map.search.createForm())
|
||||||
@ -78,9 +80,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = 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) {
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
for (element in document.select(map.search.manga_css)) {
|
for (element in document.select(map.search.manga_css)) {
|
||||||
Manga.create(id).apply {
|
Manga.create(id).apply {
|
||||||
@ -184,5 +186,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
throw Exception("image_regex and image_css are null")
|
throw Exception("image_regex and image_css are null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,21 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}"
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
private fun getFilterParams(filters: List<Filter>): String = filters
|
||||||
|
.map {
|
||||||
|
";i" + it.id
|
||||||
|
}.joinToString()
|
||||||
|
|
||||||
|
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>) {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
for (element in document.select(searchMangaSelector())) {
|
for (element in document.select(searchMangaSelector())) {
|
||||||
Manga.create(id).apply {
|
Manga.create(id).apply {
|
||||||
@ -96,7 +108,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
|
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +223,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
val start = pageUrl.indexOf("#") + 1
|
val start = pageUrl.indexOf("#") + 1
|
||||||
val end = pageUrl.indexOf("_", start)
|
val end = pageUrl.indexOf("_", start)
|
||||||
val id = pageUrl.substring(start, end)
|
val id = pageUrl.substring(start, end)
|
||||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", pageHeaders)
|
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String {
|
override fun imageUrlParse(document: Document): String {
|
||||||
@ -219,10 +231,10 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) =
|
override fun login(username: String, password: String) =
|
||||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.flatMap { doLogin(it, username, password) }
|
.flatMap { doLogin(it, username, password) }
|
||||||
.map { isAuthenticationSuccessful(it) }
|
.map { isAuthenticationSuccessful(it) }
|
||||||
|
|
||||||
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
||||||
val doc = response.asJsoup()
|
val doc = response.asJsoup()
|
||||||
@ -242,7 +254,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
override fun isAuthenticationSuccessful(response: Response) =
|
||||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
override fun isLogged(): Boolean {
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||||
@ -264,4 +276,48 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [...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()}")`
|
||||||
|
// }).join(',\n')
|
||||||
|
// on https://bato.to/search
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
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")
|
||||||
|
)
|
||||||
}
|
}
|
@ -42,22 +42,34 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
val form = FormBody.Builder().apply {
|
val form = FormBody.Builder().apply {
|
||||||
add("authorArtist", "")
|
add("authorArtist", "")
|
||||||
add("mangaName", query)
|
add("mangaName", query)
|
||||||
add("status", "")
|
add("status", "")
|
||||||
add("genres", "")
|
}
|
||||||
}.build()
|
|
||||||
|
|
||||||
return POST(page.url, headers, form)
|
val filterIndexes = filters.map { it.id.toInt() }
|
||||||
|
val maxFilterIndex = filterIndexes.max()
|
||||||
|
|
||||||
|
if (maxFilterIndex !== null) {
|
||||||
|
for (i in 0..maxFilterIndex) {
|
||||||
|
form.add("genres", if (filterIndexes.contains(i)) {
|
||||||
|
"1"
|
||||||
|
} else {
|
||||||
|
"0"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return POST(page.url, headers, form.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/AdvanceSearch"
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
@ -73,7 +85,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
||||||
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
||||||
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
||||||
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it)}
|
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,10 +121,59 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Not used
|
// Not used
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
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")
|
||||||
|
)
|
||||||
}
|
}
|
@ -36,8 +36,8 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
|
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
||||||
|
|
||||||
@ -118,4 +118,43 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
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")
|
||||||
|
)
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||||
|
@ -47,7 +47,7 @@ class Mangasee(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
|
override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query"
|
"$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.row > div > div > div > h1"
|
override fun searchMangaSelector() = "div.row > div > div > div > h1"
|
||||||
|
@ -6,8 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -38,16 +40,16 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/search"
|
"$baseUrl/search"
|
||||||
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder = okhttp3.FormBody.Builder()
|
val builder = okhttp3.FormBody.Builder()
|
||||||
builder.add("query", query)
|
builder.add("query", query)
|
||||||
|
|
||||||
return POST(page.url, headers, builder.build())
|
return POST(page.url, headers, builder.build())
|
||||||
|
@ -36,7 +36,7 @@ class WieManga(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override fun popularMangaNextPageSelector() = null
|
override fun popularMangaNextPageSelector() = null
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search/?wd=$query"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".searchresult td > div"
|
override fun searchMangaSelector() = ".searchresult td > div"
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/?do=search&subaction=search&story=$query"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
override fun popularMangaSelector() = "div.content_row"
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search?q=$query"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search?q=$query"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.catalogue
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v7.widget.GridLayoutManager
|
import android.support.v7.widget.GridLayoutManager
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
import android.support.v7.widget.SearchView
|
import android.support.v7.widget.SearchView
|
||||||
import android.support.v7.widget.Toolbar
|
import android.support.v7.widget.Toolbar
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.snack
|
|||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import eu.kanade.tachiyomi.widget.DividerItemDecoration
|
import eu.kanade.tachiyomi.widget.DividerItemDecoration
|
||||||
import eu.kanade.tachiyomi.widget.EndlessScrollListener
|
import eu.kanade.tachiyomi.widget.EndlessScrollListener
|
||||||
|
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||||
import kotlinx.android.synthetic.main.fragment_catalogue.*
|
import kotlinx.android.synthetic.main.fragment_catalogue.*
|
||||||
import kotlinx.android.synthetic.main.toolbar.*
|
import kotlinx.android.synthetic.main.toolbar.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
@ -64,7 +65,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
/**
|
/**
|
||||||
* Query of the search box.
|
* Query of the search box.
|
||||||
*/
|
*/
|
||||||
private val query: String?
|
private val query: String
|
||||||
get() = presenter.query
|
get() = presenter.query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,11 +93,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*/
|
*/
|
||||||
private var numColumnsSubscription: Subscription? = null
|
private var numColumnsSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Display mode of the catalogue (list or grid mode).
|
|
||||||
*/
|
|
||||||
private var displayMode: MenuItem? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search item.
|
* Search item.
|
||||||
*/
|
*/
|
||||||
@ -144,7 +140,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
catalogue_list.adapter = adapter
|
catalogue_list.adapter = adapter
|
||||||
catalogue_list.layoutManager = llm
|
catalogue_list.layoutManager = llm
|
||||||
catalogue_list.addOnScrollListener(listScrollListener)
|
catalogue_list.addOnScrollListener(listScrollListener)
|
||||||
catalogue_list.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
|
catalogue_list.addItemDecoration(
|
||||||
|
DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable)))
|
||||||
|
|
||||||
if (presenter.isListMode) {
|
if (presenter.isListMode) {
|
||||||
switcher.showNext()
|
switcher.showNext()
|
||||||
@ -166,28 +163,25 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
android.R.layout.simple_spinner_item, presenter.sources)
|
android.R.layout.simple_spinner_item, presenter.sources)
|
||||||
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
|
spinnerAdapter.setDropDownViewResource(R.layout.spinner_item)
|
||||||
|
|
||||||
val onItemSelected = object : AdapterView.OnItemSelectedListener {
|
val onItemSelected = IgnoreFirstSpinnerListener { position ->
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
val source = spinnerAdapter.getItem(position)
|
||||||
val source = spinnerAdapter.getItem(position)
|
if (!presenter.isValidSource(source)) {
|
||||||
if (!presenter.isValidSource(source)) {
|
spinner.setSelection(selectedIndex)
|
||||||
spinner.setSelection(selectedIndex)
|
context.toast(R.string.source_requires_login)
|
||||||
context.toast(R.string.source_requires_login)
|
} else if (source != presenter.source) {
|
||||||
} else if (source != presenter.source) {
|
selectedIndex = position
|
||||||
selectedIndex = position
|
showProgressBar()
|
||||||
showProgressBar()
|
glm.scrollToPositionWithOffset(0, 0)
|
||||||
glm.scrollToPositionWithOffset(0, 0)
|
llm.scrollToPositionWithOffset(0, 0)
|
||||||
llm.scrollToPositionWithOffset(0, 0)
|
presenter.setActiveSource(source)
|
||||||
presenter.setActiveSource(source)
|
activity.invalidateOptionsMenu()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedIndex = presenter.sources.indexOf(presenter.source)
|
||||||
|
|
||||||
spinner = Spinner(themedContext).apply {
|
spinner = Spinner(themedContext).apply {
|
||||||
adapter = spinnerAdapter
|
adapter = spinnerAdapter
|
||||||
selectedIndex = presenter.sources.indexOf(presenter.source)
|
|
||||||
setSelection(selectedIndex)
|
setSelection(selectedIndex)
|
||||||
onItemSelectedListener = onItemSelected
|
onItemSelectedListener = onItemSelected
|
||||||
}
|
}
|
||||||
@ -205,7 +199,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
searchItem = menu.findItem(R.id.action_search).apply {
|
searchItem = menu.findItem(R.id.action_search).apply {
|
||||||
val searchView = actionView as SearchView
|
val searchView = actionView as SearchView
|
||||||
|
|
||||||
if (!query.isNullOrEmpty()) {
|
if (!query.isBlank()) {
|
||||||
expandActionView()
|
expandActionView()
|
||||||
searchView.setQuery(query, true)
|
searchView.setQuery(query, true)
|
||||||
searchView.clearFocus()
|
searchView.clearFocus()
|
||||||
@ -223,20 +217,31 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup filters button
|
||||||
|
menu.findItem(R.id.action_set_filter).apply {
|
||||||
|
if (presenter.source.filters.isEmpty()) {
|
||||||
|
isEnabled = false
|
||||||
|
icon.alpha = 128
|
||||||
|
} else {
|
||||||
|
isEnabled = true
|
||||||
|
icon.alpha = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show next display mode
|
// Show next display mode
|
||||||
displayMode = menu.findItem(R.id.action_display_mode).apply {
|
menu.findItem(R.id.action_display_mode).apply {
|
||||||
val icon = if (presenter.isListMode)
|
val icon = if (presenter.isListMode)
|
||||||
R.drawable.ic_view_module_white_24dp
|
R.drawable.ic_view_module_white_24dp
|
||||||
else
|
else
|
||||||
R.drawable.ic_view_list_white_24dp
|
R.drawable.ic_view_list_white_24dp
|
||||||
setIcon(icon)
|
setIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_display_mode -> swapDisplayMode()
|
R.id.action_display_mode -> swapDisplayMode()
|
||||||
|
R.id.action_set_filter -> showFiltersDialog()
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -312,7 +317,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
*/
|
*/
|
||||||
fun onAddPage(page: Int, mangas: List<Manga>) {
|
fun onAddPage(page: Int, mangas: List<Manga>) {
|
||||||
hideProgressBar()
|
hideProgressBar()
|
||||||
if (page == 0) {
|
if (page == 1) {
|
||||||
adapter.clear()
|
adapter.clear()
|
||||||
gridScrollListener.resetScroll()
|
gridScrollListener.resetScroll()
|
||||||
listScrollListener.resetScroll()
|
listScrollListener.resetScroll()
|
||||||
@ -329,10 +334,10 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
hideProgressBar()
|
hideProgressBar()
|
||||||
Timber.e(error, error.message)
|
Timber.e(error, error.message)
|
||||||
|
|
||||||
catalogue_view.snack(error.message ?: "") {
|
catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) {
|
||||||
setAction(R.string.action_retry) {
|
setAction(R.string.action_retry) {
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
presenter.retryPage()
|
presenter.requestNext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,11 +357,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
fun swapDisplayMode() {
|
fun swapDisplayMode() {
|
||||||
presenter.swapDisplayMode()
|
presenter.swapDisplayMode()
|
||||||
val isListMode = presenter.isListMode
|
val isListMode = presenter.isListMode
|
||||||
val icon = if (isListMode)
|
activity.invalidateOptionsMenu()
|
||||||
R.drawable.ic_view_module_white_24dp
|
|
||||||
else
|
|
||||||
R.drawable.ic_view_list_white_24dp
|
|
||||||
displayMode?.setIcon(icon)
|
|
||||||
switcher.showNext()
|
switcher.showNext()
|
||||||
if (!isListMode) {
|
if (!isListMode) {
|
||||||
// Initialize mangas if going to grid view
|
// Initialize mangas if going to grid view
|
||||||
@ -444,4 +445,27 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
|
||||||
|
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] }
|
||||||
|
showProgressBar()
|
||||||
|
presenter.setSourceFilter(newFilters)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.positiveText(android.R.string.ok)
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||||
|
import rx.Observable
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
|
class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter>) {
|
||||||
|
|
||||||
|
private var lastPage: MangasPage? = null
|
||||||
|
|
||||||
|
private val results = PublishSubject.create<MangasPage>()
|
||||||
|
|
||||||
|
fun results(): Observable<MangasPage> {
|
||||||
|
return results.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
|
||||||
|
val lastPage = lastPage
|
||||||
|
|
||||||
|
val page = if (lastPage == null)
|
||||||
|
MangasPage(1)
|
||||||
|
else
|
||||||
|
MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
|
||||||
|
|
||||||
|
val observable = if (query.isBlank() && filters.isEmpty())
|
||||||
|
source.fetchPopularManga(page)
|
||||||
|
else
|
||||||
|
source.fetchSearchManga(page, query, filters)
|
||||||
|
|
||||||
|
return transformer(observable)
|
||||||
|
.doOnNext { results.onNext(it) }
|
||||||
|
.doOnNext { this@CataloguePager.lastPage = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasNextPage(): Boolean {
|
||||||
|
return lastPage == null || lastPage?.nextPageUrl != null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,9 +12,10 @@ import eu.kanade.tachiyomi.data.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.RxPager
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
@ -64,14 +65,14 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pager containing a list of manga results.
|
* Active filters.
|
||||||
*/
|
*/
|
||||||
private var pager = RxPager<Manga>()
|
var filters: List<Filter> = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last fetched page from network.
|
* Pager containing a list of manga results.
|
||||||
*/
|
*/
|
||||||
private var lastMangasPage: MangasPage? = null
|
private lateinit var pager: CataloguePager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subject that initializes a list of manga.
|
* Subject that initializes a list of manga.
|
||||||
@ -84,27 +85,20 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
var isListMode: Boolean = false
|
var isListMode: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
companion object {
|
/**
|
||||||
/**
|
* Subscription for the pager.
|
||||||
* Id of the restartable that delivers a list of manga.
|
*/
|
||||||
*/
|
private var pagerSubscription: Subscription? = null
|
||||||
const val PAGER = 1
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that requests a page of manga from network.
|
* Subscription for one request from the pager.
|
||||||
*/
|
*/
|
||||||
const val REQUEST_PAGE = 2
|
private var pageSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable that initializes the details of manga.
|
* Subscription to initialize manga details.
|
||||||
*/
|
*/
|
||||||
const val GET_MANGA_DETAILS = 3
|
private var initializerSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Key to save and restore [query] from a [Bundle].
|
|
||||||
*/
|
|
||||||
const val QUERY_KEY = "query_key"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
@ -112,52 +106,68 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
source = getLastUsedSource()
|
source = getLastUsedSource()
|
||||||
|
|
||||||
if (savedState != null) {
|
if (savedState != null) {
|
||||||
query = savedState.getString(QUERY_KEY, "")
|
query = savedState.getString(CataloguePresenter::query.name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
startableLatestCache(GET_MANGA_DETAILS,
|
|
||||||
{ mangaDetailSubject.observeOn(Schedulers.io())
|
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { !it.initialized }
|
|
||||||
.concatMap { getMangaDetailsObservable(it) }
|
|
||||||
.onBackpressureBuffer()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()) },
|
|
||||||
{ view, manga -> view.onMangaInitialized(manga) },
|
|
||||||
{ view, error -> Timber.e(error.message) })
|
|
||||||
|
|
||||||
add(prefs.catalogueAsList().asObservable()
|
add(prefs.catalogueAsList().asObservable()
|
||||||
.subscribe { setDisplayMode(it) })
|
.subscribe { setDisplayMode(it) })
|
||||||
|
|
||||||
startableReplay(PAGER,
|
restartPager()
|
||||||
{ pager.results() },
|
|
||||||
{ view, pair -> view.onAddPage(pair.first, pair.second) })
|
|
||||||
|
|
||||||
startableFirst(REQUEST_PAGE,
|
|
||||||
{ pager.request { page -> getMangasPageObservable(page + 1) } },
|
|
||||||
{ view, next -> },
|
|
||||||
{ view, error -> view.onAddPageError(error) })
|
|
||||||
|
|
||||||
start(PAGER)
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSave(state: Bundle) {
|
override fun onSave(state: Bundle) {
|
||||||
state.putString(QUERY_KEY, query)
|
state.putString(CataloguePresenter::query.name, query)
|
||||||
super.onSave(state)
|
super.onSave(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display mode.
|
* Restarts the pager for the active source with the provided query and filters.
|
||||||
*
|
*
|
||||||
* @param asList whether the current mode is in list or not.
|
* @param query the query.
|
||||||
|
* @param filters the list of active filters (for search mode).
|
||||||
*/
|
*/
|
||||||
private fun setDisplayMode(asList: Boolean) {
|
fun restartPager(query: String = this.query, filters: List<Filter> = this.filters) {
|
||||||
isListMode = asList
|
this.query = query
|
||||||
if (asList) {
|
this.filters = filters
|
||||||
stop(GET_MANGA_DETAILS)
|
|
||||||
} else {
|
if (!isListMode) {
|
||||||
start(GET_MANGA_DETAILS)
|
subscribeToMangaInitializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new pager.
|
||||||
|
pager = CataloguePager(source, query, filters)
|
||||||
|
|
||||||
|
// Prepare the pager.
|
||||||
|
pagerSubscription?.let { remove(it) }
|
||||||
|
pagerSubscription = pager.results()
|
||||||
|
.subscribeReplay({ view, page ->
|
||||||
|
view.onAddPage(page.page, page.mangas)
|
||||||
|
}, { view, error ->
|
||||||
|
Timber.e(error, error.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Request first page.
|
||||||
|
requestNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the next page for the active pager.
|
||||||
|
*/
|
||||||
|
fun requestNext() {
|
||||||
|
if (!hasNextPage()) return
|
||||||
|
|
||||||
|
pageSubscription?.let { remove(it) }
|
||||||
|
pageSubscription = pager.requestNext { getPageTransformer(it) }
|
||||||
|
.subscribeFirst({ view, page ->
|
||||||
|
// Nothing to do when onNext is emitted.
|
||||||
|
}, CatalogueFragment::onAddPageError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the last fetched page has a next page.
|
||||||
|
*/
|
||||||
|
fun hasNextPage(): Boolean {
|
||||||
|
return pager.hasNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,73 +178,64 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
fun setActiveSource(source: OnlineSource) {
|
fun setActiveSource(source: OnlineSource) {
|
||||||
prefs.lastUsedCatalogueSource().set(source.id)
|
prefs.lastUsedCatalogueSource().set(source.id)
|
||||||
this.source = source
|
this.source = source
|
||||||
restartPager()
|
|
||||||
|
restartPager(query = "", filters = emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts the request for the active source.
|
* Sets the display mode.
|
||||||
*
|
*
|
||||||
* @param query the query, or null if searching popular manga.
|
* @param asList whether the current mode is in list or not.
|
||||||
*/
|
*/
|
||||||
fun restartPager(query: String = "") {
|
private fun setDisplayMode(asList: Boolean) {
|
||||||
this.query = query
|
isListMode = asList
|
||||||
stop(REQUEST_PAGE)
|
if (asList) {
|
||||||
lastMangasPage = null
|
initializerSubscription?.let { remove(it) }
|
||||||
|
} else {
|
||||||
if (!isListMode) {
|
subscribeToMangaInitializer()
|
||||||
start(GET_MANGA_DETAILS)
|
|
||||||
}
|
|
||||||
start(PAGER)
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the next page for the active pager.
|
|
||||||
*/
|
|
||||||
fun requestNext() {
|
|
||||||
if (hasNextPage()) {
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the last fetched page has a next page.
|
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||||
*/
|
*/
|
||||||
fun hasNextPage(): Boolean {
|
private fun subscribeToMangaInitializer() {
|
||||||
return lastMangasPage?.nextPageUrl != null
|
initializerSubscription?.let { remove(it) }
|
||||||
}
|
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
/**
|
.filter { !it.initialized }
|
||||||
* Retries the current request that failed.
|
.concatMap { getMangaDetailsObservable(it) }
|
||||||
*/
|
.onBackpressureBuffer()
|
||||||
fun retryPage() {
|
|
||||||
start(REQUEST_PAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable of the network request for a page.
|
|
||||||
*
|
|
||||||
* @param page the page number to request.
|
|
||||||
* @return an observable of the network request.
|
|
||||||
*/
|
|
||||||
private fun getMangasPageObservable(page: Int): Observable<List<Manga>> {
|
|
||||||
val nextMangasPage = MangasPage(page)
|
|
||||||
if (page != 1) {
|
|
||||||
nextMangasPage.url = lastMangasPage!!.nextPageUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val observable = if (query.isEmpty())
|
|
||||||
source.fetchPopularManga(nextMangasPage)
|
|
||||||
else
|
|
||||||
source.fetchSearchManga(nextMangasPage, query)
|
|
||||||
|
|
||||||
return observable.subscribeOn(Schedulers.io())
|
|
||||||
.doOnNext { lastMangasPage = it }
|
|
||||||
.flatMap { Observable.from(it.mangas) }
|
|
||||||
.map { networkToLocalManga(it) }
|
|
||||||
.toList()
|
|
||||||
.doOnNext { initializeMangas(it) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ manga ->
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
view?.onMangaInitialized(manga)
|
||||||
|
}, { error ->
|
||||||
|
Timber.e(error, error.message)
|
||||||
|
})
|
||||||
|
.apply { add(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the function to apply to the observable of the list of manga from the source.
|
||||||
|
*
|
||||||
|
* @param observable the observable from the source.
|
||||||
|
* @return the function to apply.
|
||||||
|
*/
|
||||||
|
fun getPageTransformer(observable: Observable<MangasPage>): Observable<MangasPage> {
|
||||||
|
return observable.subscribeOn(Schedulers.io())
|
||||||
|
.doOnNext { it.mangas.replace { networkToLocalManga(it) } }
|
||||||
|
.doOnNext { initializeMangas(it.mangas) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces an object in the list with another.
|
||||||
|
*/
|
||||||
|
fun <T> MutableList<T>.replace(block: (T) -> T) {
|
||||||
|
forEachIndexed { i, obj ->
|
||||||
|
set(i, block(obj))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -354,4 +355,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
|||||||
prefs.catalogueAsList().set(!isListMode)
|
prefs.catalogueAsList().set(!isListMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active filters for the current source.
|
||||||
|
*
|
||||||
|
* @param selectedFilters a list of active filters.
|
||||||
|
*/
|
||||||
|
fun setSourceFilter(selectedFilters: List<Filter>) {
|
||||||
|
restartPager(filters = selectedFilters)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.util
|
|
||||||
|
|
||||||
import android.util.Pair
|
|
||||||
import rx.Observable
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
|
|
||||||
class RxPager<T> {
|
|
||||||
|
|
||||||
private val results = PublishSubject.create<List<T>>()
|
|
||||||
private var requestedCount: Int = 0
|
|
||||||
|
|
||||||
fun results(): Observable<Pair<Int, List<T>>> {
|
|
||||||
requestedCount = 0
|
|
||||||
return results.map { Pair(requestedCount++, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun request(networkObservable: (Int) -> Observable<List<T>>) =
|
|
||||||
networkObservable(requestedCount).doOnNext { results.onNext(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +1,55 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<android.support.design.widget.CoordinatorLayout
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:layout_height="match_parent">
|
||||||
android:orientation="vertical"
|
|
||||||
android:id="@+id/catalogue_view"
|
|
||||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
|
|
||||||
|
|
||||||
<ProgressBar
|
<LinearLayout
|
||||||
android:id="@+id/progress"
|
android:layout_width="match_parent"
|
||||||
style="?android:attr/progressBarStyleLarge"
|
android:layout_height="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:fitsSystemWindows="true"
|
||||||
android:layout_height="match_parent"
|
android:orientation="vertical"
|
||||||
android:layout_gravity="center_vertical|center_horizontal"
|
android:id="@+id/catalogue_view"
|
||||||
android:visibility="gone"/>
|
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
<ViewSwitcher
|
android:id="@+id/progress"
|
||||||
android:id="@+id/switcher"
|
style="?android:attr/progressBarStyleLarge"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1">
|
|
||||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
|
||||||
android:id="@+id/catalogue_grid"
|
|
||||||
style="@style/Theme.Widget.GridView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:columnWidth="140dp"
|
android:layout_gravity="center_vertical|center_horizontal"
|
||||||
tools:listitem="@layout/item_catalogue_grid"/>
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
|
||||||
android:id="@+id/catalogue_list"
|
<ViewSwitcher
|
||||||
|
android:id="@+id/switcher"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
|
android:id="@+id/catalogue_grid"
|
||||||
|
style="@style/Theme.Widget.GridView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:columnWidth="140dp"
|
||||||
|
tools:listitem="@layout/item_catalogue_grid"/>
|
||||||
|
|
||||||
</ViewSwitcher>
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/catalogue_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
<ProgressBar
|
</ViewSwitcher>
|
||||||
android:id="@+id/progress_grid"
|
|
||||||
style="?android:attr/progressBarStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical|center_horizontal"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_grid"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|center_horizontal"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
@ -9,6 +9,12 @@
|
|||||||
app:showAsAction="collapseActionView|ifRoom"
|
app:showAsAction="collapseActionView|ifRoom"
|
||||||
app:actionViewClass="android.support.v7.widget.SearchView"/>
|
app:actionViewClass="android.support.v7.widget.SearchView"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_set_filter"
|
||||||
|
android:title="@string/action_set_filter"
|
||||||
|
android:icon="@drawable/ic_filter_list_white_24dp"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_display_mode"
|
android:id="@+id/action_display_mode"
|
||||||
android:title="@string/action_display_mode"
|
android:title="@string/action_display_mode"
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
<string name="action_resume">Resume</string>
|
<string name="action_resume">Resume</string>
|
||||||
<string name="action_open_in_browser">Open in browser</string>
|
<string name="action_open_in_browser">Open in browser</string>
|
||||||
<string name="action_display_mode">Change display mode</string>
|
<string name="action_display_mode">Change display mode</string>
|
||||||
|
<string name="action_set_filter">Set filter</string>
|
||||||
<string name="action_cancel">Cancel</string>
|
<string name="action_cancel">Cancel</string>
|
||||||
<string name="action_sort">Sort</string>
|
<string name="action_sort">Sort</string>
|
||||||
<string name="action_install">Install</string>
|
<string name="action_install">Install</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user